diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3713077..70ff3c4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -46,6 +46,7 @@ android { packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" + resources.merges.add("META-INF/DEPENDENCIES") } } } @@ -61,6 +62,8 @@ dependencies { implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) implementation(libs.androidx.compose.material) + implementation(libs.androidx.datastore.preferences.core.jvm) + implementation(libs.androidx.espresso.core) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) @@ -70,40 +73,41 @@ dependencies { debugImplementation(libs.androidx.ui.test.manifest) implementation ("androidx.compose.material:material-icons-extended:1.5.0") - implementation ("androidx.work:work-runtime-ktx:2.7.1") +// implementation ("androidx.work:work-runtime-ktx:2.7.1") // DataStore implementation ("androidx.datastore:datastore-preferences:1.0.0") implementation ("androidx.datastore:datastore-core:1.0.0") - implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") - - implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") // Version before 1.6.0 - - implementation ("androidx.work:work-runtime-ktx:2.7.1") - +// implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") +// +// implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") // Version before 1.6.0 +// +// implementation ("androidx.work:work-runtime-ktx:2.7.1") + implementation ("androidx.core:core-ktx:1.10.1") + implementation ("androidx.navigation:navigation-compose:2.8.3") +// implementation ("com.jcraft:jsch:0.1.55") +// +// implementation ("org.apache.sshd:sshd-core:2.9.0") // implementation(libs.charts) - +// implementation ("com.github.peerlab:socks5:1.0.0") implementation (libs.kotlinx.serialization.json) // implementation ("com.github.bumptech.glide:glide:4.15.1") implementation(libs.timber) -// implementation (libs.mpandroidchart) + implementation (libs.okhttp) + implementation (libs.nanohttpd) +// implementation (libs.littleproxy) +// implementation ("org.littleshoot:littleproxy:2.0.0-beta6") +// implementation (libs.netty.all) -// implementation ("androidx.core:core-ktx:1.12.0") -// implementation ("androidx.activity:activity-compose:1.7.2") -// implementation ("androidx.compose.ui:ui:1.5.0") -// implementation ("androidx.compose.material:material:1.5.0") -// implementation ("androidx.compose.ui:ui-tooling-preview:1.5.0") -// implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") -// implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index acdfd60..4c7665e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,17 +16,31 @@ + + + + + + + + + + + + + @@ -47,14 +61,5 @@ - - - - - - - - - diff --git a/app/src/main/java/com/example/wifip2photspot/BandSelection.kt b/app/src/main/java/com/example/wifip2photspot/BandSelection.kt index 55124c1..6abe93b 100644 --- a/app/src/main/java/com/example/wifip2photspot/BandSelection.kt +++ b/app/src/main/java/com/example/wifip2photspot/BandSelection.kt @@ -1,62 +1,55 @@ package com.example.wifip2photspot -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.* -import androidx.compose.runtime.* - +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import androidx.compose.foundation.layout.padding - -//@Composable -//fun BandSelection( -// selectedBand: String, -// onBandSelected: (String) -> Unit, -// bands: List, -// isHotspotEnabled: Boolean -//) { -// Card( -// elevation = CardDefaults.cardElevation(4.dp), -// shape = MaterialTheme.shapes.medium, -// modifier = Modifier -// .fillMaxWidth() -// .padding(vertical = 8.dp) -// ) { -// Column(modifier = Modifier.padding(16.dp)) { -// Text( -// text = "Select Band", -// style = MaterialTheme.typography.titleMedium, -// modifier = Modifier.padding(bottom = 8.dp) -// ) -// Row( -// horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth() -// ) { -// bands.forEach { band -> -// OutlinedButton( -// onClick = { onBandSelected(band) }, -// colors = ButtonDefaults.outlinedButtonColors( -// containerColor = if (selectedBand == band) MaterialTheme.colorScheme.primary.copy( -// alpha = 0.1f -// ) else Color.Transparent -// ), -// enabled = !isHotspotEnabled, -// modifier = Modifier -// .weight(1f) -// .padding(horizontal = 4.dp) -// ) { -// Text( -// text = band, -// color = if (selectedBand == band) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface -// ) -// } -// } -// } -// } -// } -//} - +@Composable +fun BandSelection( + selectedBand: String, + onBandSelected: (String) -> Unit, + bands: List, + isHotspotEnabled: Boolean +) { + Column(modifier = Modifier.padding(8.dp)) { + Text("Wi-Fi Band Selection", style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + bands.forEach { band -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = isHotspotEnabled.not()) { onBandSelected(band) } + .padding(vertical = 4.dp) + ) { + RadioButton( + selected = selectedBand == band, + onClick = { onBandSelected(band) }, + enabled = isHotspotEnabled.not() + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(band, style = MaterialTheme.typography.bodyLarge) + } + } + if (isHotspotEnabled) { + Text( + text = "Cannot change band while hotspot is active.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), + modifier = Modifier.padding(top = 4.dp) + ) + } + } +} diff --git a/app/src/main/java/com/example/wifip2photspot/BlockedDevicesSection.kt b/app/src/main/java/com/example/wifip2photspot/BlockedDevicesSection.kt index e4d87b3..917c6d2 100644 --- a/app/src/main/java/com/example/wifip2photspot/BlockedDevicesSection.kt +++ b/app/src/main/java/com/example/wifip2photspot/BlockedDevicesSection.kt @@ -1,67 +1,24 @@ package com.example.wifip2photspot -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items -import androidx.compose.material3.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.Info -import androidx.compose.material.icons.filled.Smartphone +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.Alignment -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -@Composable -fun BlockedDevicesSection( - devices: List, - onUnblock: (String) -> Unit -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - ) { - Text( - text = "Blocked Devices (${devices.size}):", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(vertical = 8.dp) - ) - - if (devices.isEmpty()) { - Text( - text = "No blocked devices.", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(vertical = 8.dp) - ) - } else { - LazyColumn( - modifier = Modifier.fillMaxWidth() - ) { - items(devices) { deviceInfo -> - BlockedDeviceCard( - deviceInfo = deviceInfo, - onUnblock = onUnblock - ) - Divider() - } - } - } - } -} // BlockedDevicesSection.kt fun LazyListScope.blockedDevicesSection( devices: List, @@ -90,7 +47,7 @@ fun LazyListScope.blockedDevicesSection( deviceInfo = deviceInfo, onUnblock = onUnblock ) - Divider() + HorizontalDivider() } } } diff --git a/app/src/main/java/com/example/wifip2photspot/ConnectedDevicesSection.kt b/app/src/main/java/com/example/wifip2photspot/ConnectedDevicesSection.kt index 5da7686..e8c5fa2 100644 --- a/app/src/main/java/com/example/wifip2photspot/ConnectedDevicesSection.kt +++ b/app/src/main/java/com/example/wifip2photspot/ConnectedDevicesSection.kt @@ -1,124 +1,41 @@ package com.example.wifip2photspot -import android.app.TimePickerDialog -import android.content.Context -import android.net.wifi.p2p.WifiP2pDevice - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items -import androidx.compose.material3.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.Info -import androidx.compose.material.icons.filled.Smartphone +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.Alignment -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import java.text.SimpleDateFormat -import java.util.Calendar import java.util.Date 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, @@ -218,17 +135,6 @@ fun formatTime(timestamp: Long): String { return sdf.format(Date(timestamp)) } -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" - } -} -// ConnectedDevicesSection.kt fun LazyListScope.connectedDevicesSection( devices: List, onDeviceAliasChange: (String, String) -> Unit, @@ -262,152 +168,7 @@ fun LazyListScope.connectedDevicesSection( onBlockUnblock = onBlockUnblock, onDisconnect = onDisconnect ) - Divider() - } - } -} - -// -//@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") + HorizontalDivider() } } } - -// 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)) -} diff --git a/app/src/main/java/com/example/wifip2photspot/ConnectionStatusBar.kt b/app/src/main/java/com/example/wifip2photspot/ConnectionStatusBar.kt index d7d1eac..094ae7a 100644 --- a/app/src/main/java/com/example/wifip2photspot/ConnectionStatusBar.kt +++ b/app/src/main/java/com/example/wifip2photspot/ConnectionStatusBar.kt @@ -1,37 +1,42 @@ // ConnectionStatusBar.kt package com.example.wifip2photspot -import android.provider.CalendarContract.Colors -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.* -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateIntAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.DeviceHub import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Upload +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import com.github.mikephil.charting.charts.LineChart -import com.github.mikephil.charting.components.XAxis -import com.github.mikephil.charting.data.Entry -import com.github.mikephil.charting.data.LineData -import com.github.mikephil.charting.data.LineDataSet @Composable fun ConnectionStatusBar( @@ -190,7 +195,7 @@ fun AnimatedText( val targetValue = text.toIntOrNull() ?: 0 val animatedValue by animateIntAsState( targetValue = targetValue, - animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing) + animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing), label = "" ) Row(verticalAlignment = Alignment.CenterVertically) { @@ -205,45 +210,7 @@ fun AnimatedText( ) } } -@Composable -fun AnimatedButton( - text: String, - onClick: () -> Unit, - enabled: Boolean, - modifier: Modifier = Modifier -) { - // Animation for scaling the button when pressed - var isPressed by remember { mutableStateOf(false) } - val scale by animateFloatAsState( - targetValue = if (isPressed) 0.95f else 1f, - animationSpec = tween(durationMillis = 100) - ) - // Animation for color change - val buttonColor by animateColorAsState( - targetValue = if (enabled) MaterialTheme.colorScheme.primary else Color.Gray, - animationSpec = tween(durationMillis = 300) - ) - - Button( - onClick = onClick, - enabled = enabled, - modifier = modifier - .scale(scale) - .pointerInput(Unit) { - detectTapGestures( - onPress = { - isPressed = true - tryAwaitRelease() - isPressed = false - } - ) - }, - colors = ButtonDefaults.buttonColors(containerColor = buttonColor) - ) { - Text(text) - } -} @Composable @@ -280,66 +247,50 @@ fun MetricSubCard( } @Composable -fun DataUsageSection( - rxBytes: Long, - txBytes: Long -) { +fun BatteryStatusSection(batteryLevel: Int) { Column(modifier = Modifier.padding(16.dp)) { - Text("Session Data Usage", style = MaterialTheme.typography.titleMedium) + Text("Battery Status", style = MaterialTheme.typography.titleMedium) Spacer(modifier = Modifier.height(8.dp)) - Text("Download: ${formatBytes(rxBytes)}") - Text("Upload: ${formatBytes(txBytes)}") - } -} - -fun formatBytes(bytes: Long): String { - val kb = bytes / 1024 - val mb = kb / 1024 - return when { - mb > 0 -> "$mb MB" - kb > 0 -> "$kb KB" - else -> "$bytes Bytes" + Text("Battery Level: $batteryLevel%") } } -//@Composable -//fun HistoricalDataUsageSection(historicalData: List) { -// Column(modifier = Modifier.padding(16.dp)) { -// Text("Historical Data Usage", style = MaterialTheme.typography.titleMedium) -// Spacer(modifier = Modifier.height(8.dp)) -// historicalData.forEach { record -> -// Text("${record.date}: Download ${formatBytes(record.rxBytes)}, Upload ${formatBytes(record.txBytes)}") -// } -// } -//} - @Composable -fun SpeedGraphSection( - uploadSpeeds: List, - downloadSpeeds: List +fun SwitchPreference( + label: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + enabled: Boolean = true ) { - AndroidView( - factory = { context -> - LineChart(context).apply { - description.isEnabled = false - xAxis.position = XAxis.XAxisPosition.BOTTOM - axisRight.isEnabled = false - } - }, - update = { chart -> - val uploadDataSet = LineDataSet(uploadSpeeds, "Upload Speed").apply { - color = Color.Red.toArgb() - } - val downloadDataSet = LineDataSet(downloadSpeeds, "Download Speed").apply { - color = Color.Green.toArgb() - } - val data = LineData(uploadDataSet, downloadDataSet) - chart.data = data - chart.invalidate() - }, + var checkedState by rememberSaveable { mutableStateOf(true) } + Row( modifier = Modifier .fillMaxWidth() - .height(200.dp) - ) + .padding(vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(label, style = MaterialTheme.typography.bodyLarge) + Switch( + checked = checked, + onCheckedChange = { + checkedState = it + onCheckedChange(it) + }, + enabled = enabled, + thumbContent = { + if (checked) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + + )} + } + ) + } } + + + diff --git a/app/src/main/java/com/example/wifip2photspot/CustomHttpProxyServer.kt b/app/src/main/java/com/example/wifip2photspot/CustomHttpProxyServer.kt new file mode 100644 index 0000000..15bee86 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/CustomHttpProxyServer.kt @@ -0,0 +1,182 @@ +package com.example.wifip2photspot + +import android.util.Log +import timber.log.Timber +import java.io.* +import java.net.ServerSocket +import java.net.Socket + +import java.io.InputStream +import java.io.OutputStream +import java.util.concurrent.Executors + +import kotlin.concurrent.thread + +class HttpProxyServer(private val port: Int, private val bindIp: String) { + + private val clientHandlingExecutor = Executors.newFixedThreadPool(50) // Adjust pool size as needed + private val dataForwardingExecutor = Executors.newCachedThreadPool() + + private var serverSocket: ServerSocket? = null + private var isRunning = false + + fun start() { + serverSocket = ServerSocket(port) + isRunning = true + + thread { + while (isRunning) { + try { + val clientSocket = serverSocket!!.accept() + clientHandlingExecutor.execute { handleClient(clientSocket) } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + } + + fun stop() { + isRunning = false + serverSocket?.close() + } + + private fun handleClient(clientSocket: Socket) { + val targetSocket: Socket? = null + + try { + val clientInput = clientSocket.getInputStream() + val clientOutput = clientSocket.getOutputStream() + + // Read the client's request + val requestBuffer = ByteArray(8192) + val bytesRead = clientInput.read(requestBuffer) + if (bytesRead == -1) { + clientSocket.close() + return + } + + val requestHeader = String(requestBuffer, 0, bytesRead) + val requestLines = requestHeader.split("\r\n") + if (requestLines.isEmpty()) { + clientSocket.close() + return + } + + val requestLine = requestLines[0] + val tokens = requestLine.split(" ") + if (tokens.size < 3) { + clientSocket.close() + return + } + + val method = tokens[0] + val uri = tokens[1] + + if (method.equals("CONNECT", ignoreCase = true)) { + // Handle HTTPS tunneling + val hostPort = uri.split(":") + val host = hostPort[0] + val port = hostPort.getOrNull(1)?.toIntOrNull() ?: 443 + + val targetSocket = Socket(host, port) + clientOutput.write("HTTP/1.1 200 Connection Established\r\n\r\n".toByteArray()) + clientOutput.flush() + + // Forward data between client and target server + // Start data forwarding threads + val clientToServer = thread { + forwardData(clientInput, targetSocket!!.getOutputStream()) + } + val serverToClient = thread { + forwardData(targetSocket!!.getInputStream(), clientOutput) + } + + + + // Wait for both threads to finish + clientToServer.join() + serverToClient.join() + targetSocket.close() + } else { + // Handle HTTP request + // Modify headers as needed + val modifiedRequestHeader = modifyHeaders(requestHeader) + + // Extract host and port from headers + val hostLine = requestLines.find { it.startsWith("Host:", ignoreCase = true) } + if (hostLine == null) { + clientSocket.close() + return + } + val hostPort = hostLine.substringAfter(" ").split(":") + val host = hostPort[0] + val port = hostPort.getOrNull(1)?.toIntOrNull() ?: 80 + + val targetSocket = Socket(host, port) + val targetOutput = targetSocket.getOutputStream() + targetOutput.write(modifiedRequestHeader.toByteArray()) + targetOutput.flush() + + // Forward data between client and target server + val clientToServer = thread { + forwardData(clientInput, targetOutput) + } + val serverToClient = thread { + forwardData(targetSocket.getInputStream(), clientOutput) + } + + clientToServer.join() + serverToClient.join() + + targetSocket.close() + } + + clientSocket.close() + } catch (e: Exception) { + e.printStackTrace() + try { + clientSocket.close() + } catch (_: Exception) { + } finally { + try { + targetSocket?.close() + } catch (_: Exception) {} + try { + clientSocket.close() + } catch (_: Exception) {} + } + } + } + + private fun modifyHeaders(requestHeader: String): String { + val lines = requestHeader.lines().toMutableList() + for (i in lines.indices) { + if (lines[i].startsWith("User-Agent:", ignoreCase = true)) { + lines[i] = "User-Agent: Mozilla/5.0 (Android)" + } + if (lines[i].startsWith("Referer:", ignoreCase = true)) { + lines[i] = "Referer: https://example.com" + } + } + return lines.joinToString("\r\n") + "\r\n\r\n" + } + + private fun forwardData(inputStream: InputStream, outputStream: OutputStream) { + try { + val buffer = ByteArray(8192) + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + outputStream.write(buffer, 0, bytesRead) + outputStream.flush() + } + } catch (e: Exception) { + Timber.tag("HttpProxyServer").e(e, "Error forwarding data: %s", e.message) + } finally { + try { + outputStream.close() + } catch (_: Exception) { + } + } + } +} diff --git a/app/src/main/java/com/example/wifip2photspot/DataStoreManager.kt b/app/src/main/java/com/example/wifip2photspot/DataStoreManager.kt deleted file mode 100644 index 6d822e9..0000000 --- a/app/src/main/java/com/example/wifip2photspot/DataStoreManager.kt +++ /dev/null @@ -1,50 +0,0 @@ -// DataStoreManager.kt -package com.example.wifip2photspot - -import android.content.Context -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import kotlinx.coroutines.flow.Flow -import androidx.datastore.preferences.core.edit -import kotlinx.coroutines.flow.map - -object DataStoreManager { - private const val DATASTORE_NAME = "hotspot_prefs" - - private val Context.dataStore by preferencesDataStore( - name = DATASTORE_NAME - ) - - // Define keys - private val SSID_KEY = stringPreferencesKey("ssid_key") - private val PASSWORD_KEY = stringPreferencesKey("password_key") - - // Function to get SSID - fun getSsid(context: Context): Flow { - return context.dataStore.data.map { preferences -> - preferences[SSID_KEY] ?: "TetherGuard" // Default SSID - } - } - - // Function to get Password - fun getPassword(context: Context): Flow { - return context.dataStore.data.map { preferences -> - preferences[PASSWORD_KEY] ?: "00000000" // Default Password - } - } - - // Function to save SSID - suspend fun saveSsid(context: Context, ssid: String) { - context.dataStore.edit { preferences -> - preferences[SSID_KEY] = ssid - } - } - - // Function to save Password - suspend fun savePassword(context: Context, password: String) { - context.dataStore.edit { preferences -> - preferences[PASSWORD_KEY] = password - } - } -} diff --git a/app/src/main/java/com/example/wifip2photspot/DataUsageRecord.kt b/app/src/main/java/com/example/wifip2photspot/DataUsageRecord.kt deleted file mode 100644 index 3d28f35..0000000 --- a/app/src/main/java/com/example/wifip2photspot/DataUsageRecord.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.wifip2photspot -// -//import java.time.LocalDate -// -//data class DataUsageRecord( -// val date: LocalDate, -// val rxBytes: Long, -// val txBytes: Long -//) \ 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..d94a63f 100644 --- a/app/src/main/java/com/example/wifip2photspot/DeviceInfo.kt +++ b/app/src/main/java/com/example/wifip2photspot/DeviceInfo.kt @@ -1,7 +1,6 @@ package com.example.wifip2photspot import android.net.wifi.p2p.WifiP2pDevice -import java.time.LocalDate // DeviceInfo.kt data class DeviceInfo( diff --git a/app/src/main/java/com/example/wifip2photspot/FeedbackForm.kt b/app/src/main/java/com/example/wifip2photspot/FeedbackForm.kt index 6b1c47e..7a483cb 100644 --- a/app/src/main/java/com/example/wifip2photspot/FeedbackForm.kt +++ b/app/src/main/java/com/example/wifip2photspot/FeedbackForm.kt @@ -1,22 +1,21 @@ package com.example.wifip2photspot -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.focus.* -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.text.input.* import androidx.compose.ui.unit.dp -import androidx.wear.compose.material.ContentAlpha @Composable fun FeedbackForm(onSubmit: (String) -> Unit) { @@ -50,7 +49,7 @@ fun FeedbackForm(onSubmit: (String) -> Unit) { @Composable fun ContactSupportSection(onContactSupport: () -> Unit) { - Column(modifier = Modifier.padding(16.dp)) { + Column(modifier = Modifier.padding(8.dp)) { Text("Need Help?", style = MaterialTheme.typography.titleMedium) Spacer(modifier = Modifier.height(8.dp)) @@ -59,3 +58,6 @@ fun ContactSupportSection(onContactSupport: () -> Unit) { } } } + + + diff --git a/app/src/main/java/com/example/wifip2photspot/HotspotControlSection.kt b/app/src/main/java/com/example/wifip2photspot/HotspotControlSection.kt index 980190d..c0de1df 100644 --- a/app/src/main/java/com/example/wifip2photspot/HotspotControlSection.kt +++ b/app/src/main/java/com/example/wifip2photspot/HotspotControlSection.kt @@ -1,33 +1,31 @@ // HotspotControlSection.kt package com.example.wifip2photspot - -import android.content.Intent -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Sync import androidx.compose.material.icons.filled.Wifi import androidx.compose.material.icons.filled.WifiOff -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import com.example.wifip2photspot.Proxy.ProxyService @Composable fun HotspotControlSection( isHotspotEnabled: Boolean, isProcessing: Boolean, - ssidInput: String, - proxyPort: Int, - passwordInput: String, - selectedBand: String, onStartTapped: () -> Unit, - onStopTapped: () -> Unit + onStopTapped: () -> Unit, ) { - val context = LocalContext.current Row( modifier = Modifier @@ -35,23 +33,20 @@ fun HotspotControlSection( .padding(vertical = 16.dp), horizontalArrangement = Arrangement.SpaceEvenly ) { + // Start Button with Loading Indicator Button( - onClick = { - onStartTapped() - // Start ProxyService - val intent = Intent(context, ProxyService::class.java) - context.startService(intent) - }, - enabled = !isHotspotEnabled && !isProcessing + onClick = onStartTapped, + enabled = !isHotspotEnabled && !isProcessing, + modifier = Modifier.weight(1f).padding(end = 8.dp) ) { if (isProcessing) { CircularProgressIndicator( modifier = Modifier.size(20.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onPrimary + color = MaterialTheme.colorScheme.onPrimary, + strokeWidth = 2.dp ) } else { - Text("Start Hotspot & Proxy") + Text("Start Hotspot") } } @@ -62,33 +57,30 @@ fun HotspotControlSection( } val statusText = when { isProcessing -> if (isHotspotEnabled) "Stopping hotspot..." else "Starting hotspot..." - isHotspotEnabled -> "Hotspot & Proxy are active" - else -> "Hotspot & Proxy are inactive" + isHotspotEnabled -> "Hotspot active" + else -> "Hotspot is inactive" } Icon( imageVector = statusIcon, contentDescription = statusText, tint = if (isHotspotEnabled) Color(0xFF4CAF50) else Color(0xFFF44336) // Green or Red ) + + // Stop Button with Loading Indicator Button( - onClick = { - onStopTapped() - // Stop ProxyService - val intent = Intent(context, ProxyService::class.java) - context.stopService(intent) - }, - enabled = isHotspotEnabled && !isProcessing + onClick = onStopTapped, + enabled = isHotspotEnabled && !isProcessing, + modifier = Modifier.weight(1f).padding(start = 8.dp) ) { if (isProcessing) { CircularProgressIndicator( modifier = Modifier.size(20.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onPrimary + color = MaterialTheme.colorScheme.onPrimary, + strokeWidth = 2.dp ) } else { - Text("Stop Hotspot & Proxy") + Text("Stop Hotspot") } } } -} - +} \ No newline at end of file diff --git a/app/src/main/java/com/example/wifip2photspot/HotspotViewModel.kt b/app/src/main/java/com/example/wifip2photspot/HotspotViewModel.kt deleted file mode 100644 index a689440..0000000 --- a/app/src/main/java/com/example/wifip2photspot/HotspotViewModel.kt +++ /dev/null @@ -1,927 +0,0 @@ -// HotspotViewModel.kt -package com.example.wifip2photspot - -import android.app.Application -import android.app.NotificationManager -import android.content.Context -import android.content.Intent -import android.net.TrafficStats -import android.net.Uri -import android.net.wifi.p2p.WifiP2pConfig -import android.net.wifi.p2p.WifiP2pDevice -import android.net.wifi.p2p.WifiP2pManager -import android.os.Build -import android.os.Looper -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.core.app.NotificationCompat -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.* -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.viewModelScope -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager -import com.github.mikephil.charting.data.Entry -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.delay -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 - -@RequiresApi(Build.VERSION_CODES.Q) -class HotspotViewModel( - application: Application, - private val dataStore: DataStore -) : AndroidViewModel(application) { - - // ----- DataStore Keys ----- - companion object { - val SSID_KEY = stringPreferencesKey("ssid") - val PASSWORD_KEY = stringPreferencesKey("password") - } - - // ----- Wi-Fi P2P Manager and Channel ----- - val wifiManager: WifiP2pManager = - application.getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager - val channel: WifiP2pManager.Channel = - wifiManager.initialize(application, Looper.getMainLooper(), null) - - - // ----- StateFlows for UI State ----- - private val _ssid = MutableStateFlow("TetherGuard") - val ssid: StateFlow = _ssid.asStateFlow() - - private val _password = MutableStateFlow("00000000") - val password: StateFlow = _password.asStateFlow() - - private val _selectedBand = MutableStateFlow("Auto") - val selectedBand: StateFlow = _selectedBand.asStateFlow() - - private val _isWifiP2pEnabled = MutableStateFlow(false) - val isWifiP2pEnabled: StateFlow = _isWifiP2pEnabled.asStateFlow() - - private val _isHotspotEnabled = MutableStateFlow(false) - val isHotspotEnabled: StateFlow = _isHotspotEnabled.asStateFlow() - - private val _isProcessing = MutableStateFlow(false) - val isProcessing: StateFlow = _isProcessing.asStateFlow() - - private val _uploadSpeed = MutableStateFlow(0) // in kbps - val uploadSpeed: StateFlow = _uploadSpeed.asStateFlow() - - private val _downloadSpeed = MutableStateFlow(0) // in kbps - val downloadSpeed: StateFlow = _downloadSpeed.asStateFlow() - - private var previousTxBytes = TrafficStats.getTotalTxBytes() - private var previousRxBytes = TrafficStats.getTotalRxBytes() - - private var sessionStartRxBytes = 0L - private var sessionStartTxBytes = 0L - - private val DATA_USAGE_KEY = stringPreferencesKey("device_usage_key") - - - // ----- Log Entries ----- - private val _logEntries = MutableStateFlow>(emptyList()) - val logEntries: StateFlow> = _logEntries.asStateFlow() - - // ----- UI Events ----- - private val _eventFlow = MutableSharedFlow() - val eventFlow: SharedFlow = _eventFlow.asSharedFlow() - - // ----- Connected Devices ----- - private val _connectedDevices = MutableStateFlow>(emptyList()) - val connectedDevices: StateFlow> = _connectedDevices.asStateFlow() - - private val _connectedDeviceInfos = MutableStateFlow>(emptyList()) - val connectedDeviceInfos: StateFlow> = _connectedDeviceInfos.asStateFlow() - - private var passwordVisible by mutableStateOf(false) - - // ----- Sealed Class for UI Events ----- - sealed class UiEvent { - data class ShowToast(val message: String) : UiEvent() - data class ShowSnackbar(val message: String) : UiEvent() - object StartProxyService : UiEvent() - object StopProxyService : UiEvent() - } - - // Proxy server properties - private val _isProxyRunning = MutableStateFlow(false) - val isProxyRunning: StateFlow = _isProxyRunning.asStateFlow() - - private val _proxyPort = MutableStateFlow(8080) - val proxyPort: StateFlow = _proxyPort.asStateFlow() - - - private val BLOCKED_MAC_ADDRESSES_KEY = stringSetPreferencesKey("blocked_mac_addresses") - - // StateFlows to hold the lists - private val _allowedMacAddresses = MutableStateFlow>(emptySet()) - - // Add a new property for blocked devices - private val _blockedMacAddresses = MutableStateFlow>(emptySet()) - val blockedMacAddresses: StateFlow> = _blockedMacAddresses.asStateFlow() - - private val _blockedDeviceInfos = MutableStateFlow>(emptyList()) - val blockedDeviceInfos: StateFlow> = _blockedDeviceInfos.asStateFlow() - - // Define DataStore key - val DEVICE_ALIAS_KEY = stringPreferencesKey("device_aliases") - - // Load aliases in init block - private val _deviceAliases = MutableStateFlow>(emptyMap()) -// -// private val _historicalDataUsage = MutableStateFlow>(emptyList()) -// val historicalDataUsage: StateFlow> = _historicalDataUsage.asStateFlow() - - private val _uploadSpeedEntries = MutableStateFlow>(emptyList()) - val uploadSpeedEntries: StateFlow> = _uploadSpeedEntries.asStateFlow() - - private val _downloadSpeedEntries = MutableStateFlow>(emptyList()) - val downloadSpeedEntries: StateFlow> = _downloadSpeedEntries.asStateFlow() - - - // Variables to store current speeds in Kbps - private val _uploadSpeedKbps = MutableStateFlow(0f) - val uploadSpeedKbps: StateFlow = _uploadSpeedKbps.asStateFlow() - - private val _downloadSpeedKbps = MutableStateFlow(0f) - val downloadSpeedKbps: StateFlow = _downloadSpeedKbps.asStateFlow() - - private val _dataUsageThreshold = MutableStateFlow(0L) // in bytes - val dataUsageThreshold: StateFlow = _dataUsageThreshold.asStateFlow() - - private val _notificationEnabled = MutableStateFlow(true) - val notificationEnabled: StateFlow = _notificationEnabled.asStateFlow() - - private val _notificationSoundEnabled = MutableStateFlow(true) - val notificationSoundEnabled: StateFlow = _notificationSoundEnabled.asStateFlow() - - private val _notificationVibrationEnabled = MutableStateFlow(true) - val notificationVibrationEnabled: StateFlow = - _notificationVibrationEnabled.asStateFlow() - - -// 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 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) -// } -// } - - - init { - - // ----- Load SSID and Password from DataStore ----- - viewModelScope.launch { - dataStore.data - .catch { exception -> - if (exception is IOException) { - emit(emptyPreferences()) - } else { - throw exception - } - } - .collect { preferences -> - _ssid.value = preferences[SSID_KEY] ?: "TetherGuard" - _password.value = preferences[PASSWORD_KEY] ?: "00000000" - } - } - // ----- Start Monitoring Network Speeds ----- -// In the coroutine where you update uploadSpeed and downloadSpeed - viewModelScope.launch { - var time = 0f - while (true) { - delay(1000) // Update every second -// - val currentTxBytes = TrafficStats.getTotalTxBytes() - val currentRxBytes = TrafficStats.getTotalRxBytes() - - val txBytesDiff = currentTxBytes - previousTxBytes - val rxBytesDiff = currentRxBytes - previousRxBytes - - previousTxBytes = currentTxBytes - previousRxBytes = currentRxBytes - - // Convert bytes to kilobits per second (kbps) - val uploadSpeedKbps = (txBytesDiff * 8) / 1000 - val downloadSpeedKbps = (rxBytesDiff * 8) / 1000 - - _uploadSpeed.value = uploadSpeedKbps.toInt() - _downloadSpeed.value = downloadSpeedKbps.toInt() - // Update entries - _uploadSpeedEntries.value += Entry(time, uploadSpeedKbps.toFloat()) - _downloadSpeedEntries.value += Entry(time, downloadSpeedKbps.toFloat()) - time += 1f - - // Limit the number of entries to, e.g., 60 - if (_uploadSpeedEntries.value.size > 60) { - _uploadSpeedEntries.value = _uploadSpeedEntries.value.drop(1) - _downloadSpeedEntries.value = _downloadSpeedEntries.value.drop(1) - } - - } - } - // Load blocked devices - viewModelScope.launch { - dataStore.data.collect { preferences -> - val blockedAddresses = preferences[BLOCKED_MAC_ADDRESSES_KEY] ?: emptySet() - _blockedMacAddresses.value = blockedAddresses - updateBlockedDevices() - } - } - // Load device aliases from DataStore - viewModelScope.launch { - dataStore.data.collect { preferences -> - val aliasesJson = preferences[DEVICE_ALIAS_KEY] ?: "{}" - _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) -// } -// } - monitorNetworkSpeeds() -// -// viewModelScope.launch { -// dataStore.data.collect { preferences -> -// _dataUsageThreshold.value = preferences[DATA_USAGE_THRESHOLD_KEY] ?: 0L -// } -// } - - } - - @Suppress("EXPERIMENTAL_API_USAGE") - private fun monitorNetworkSpeeds() { - viewModelScope.launch { - while (true) { - delay(1000L) - - val currentRxBytes = TrafficStats.getTotalRxBytes() - val currentTxBytes = TrafficStats.getTotalTxBytes() - - // Calculate the difference in bytes over the last second - val rxBytes = currentRxBytes - previousRxBytes - val txBytes = currentTxBytes - previousTxBytes - - // Update previous bytes for next calculation - previousRxBytes = currentRxBytes - previousTxBytes = currentTxBytes - - // Convert bytes to kilobits per second (Kbps) - val currentDownloadSpeedKbps = (rxBytes * 8) / 1000f - val currentUploadSpeedKbps = (txBytes * 8) / 1000f - - // Update the StateFlows - _downloadSpeedKbps.value = currentDownloadSpeedKbps - _uploadSpeedKbps.value = currentUploadSpeedKbps - - // Update entries for the graph - val time = (System.currentTimeMillis() / 1000f) // Time in seconds - - _downloadSpeedEntries.value += Entry(time, currentDownloadSpeedKbps) - _uploadSpeedEntries.value += Entry(time, currentUploadSpeedKbps) - - // Limit the number of entries to, e.g., 60 - if (_downloadSpeedEntries.value.size > 60) { - _downloadSpeedEntries.value = _downloadSpeedEntries.value.drop(1) - _uploadSpeedEntries.value = _uploadSpeedEntries.value.drop(1) - } - } - } - } - - ////////*(((((((((((***********))))))))))))///////////////////// //////////////////////////////working fine ////////////////////////////////// - - override fun onCleared() { - super.onCleared() - } - - // ----- Function to Update Log Entries ----- - fun updateLog(message: String) { - _logEntries.value += message - } - - // ----- Function to Handle Device List Changes ----- - fun onDevicesChanged(deviceList: Collection) { - val previousDevices = _connectedDevices.value - _connectedDevices.value = deviceList.toList() - enforceAccessControl() - - // Check for new connections - val newDevices = _connectedDevices.value - previousDevices - val disconnectedDevices = previousDevices - _connectedDevices.value - - newDevices.forEach { device -> - sendDeviceConnectionNotification(device, connected = true) - } - disconnectedDevices.forEach { device -> - sendDeviceConnectionNotification(device, connected = false) - } - - Log.d("HotspotViewModel", "Devices changed: ${deviceList.size} devices") - val blockedAddresses = _blockedMacAddresses.value - _connectedDeviceInfos.value = deviceList.map { device -> - val isBlocked = blockedAddresses.contains(device.deviceAddress) - val existingInfo = - _connectedDeviceInfos.value.find { it.device.deviceAddress == device.deviceAddress } - existingInfo?.copy(isBlocked = isBlocked) ?: DeviceInfo( - device = device, - isBlocked = isBlocked - ) - } - } - - - // ----- Function to Set Wi-Fi P2P Enabled State ----- - fun setWifiP2pEnabled(enabled: Boolean) { - _isWifiP2pEnabled.value = enabled - updateLog("Wi-Fi P2P Enabled: $enabled") - } - - // ----- Function to Update Selected Band ----- - fun updateSelectedBand(newBand: String) { - viewModelScope.launch { - _selectedBand.value = newBand - } - } - - // ----- Function to Update SSID ----- - fun updateSSID(newSSID: String) { - viewModelScope.launch { - dataStore.edit { preferences -> - preferences[SSID_KEY] = newSSID - } - _ssid.value = newSSID - updateLog("SSID updated to: $newSSID") - } - } - - // ----- Function to Update Password ----- - fun updatePassword(newPassword: String) { - viewModelScope.launch { - dataStore.edit { preferences -> - preferences[PASSWORD_KEY] = newPassword - } - _password.value = newPassword - updateLog("Password updated.") - } - } - - // ----- Function to Start the Hotspot ----- - fun onButtonStartTapped( - ssidInput: String, - passwordInput: String, - selectedBand: String - ) { - onButtonStopTapped() - viewModelScope.launch { - - if (!_isWifiP2pEnabled.value) { - updateLog("Error: Cannot start hotspot. Wi-Fi P2P is not enabled.") - _eventFlow.emit(UiEvent.ShowToast("Wi-Fi P2P is not enabled.")) - _isHotspotEnabled.value = false - return@launch - } - - _isProcessing.value = true // Start processing - - val ssidTrimmed = ssidInput.trim() - val passwordTrimmed = passwordInput.trim() - - // ----- Input Validation ----- - if (ssidTrimmed.isEmpty()) { - updateLog("Error: SSID cannot be empty.") - _eventFlow.emit(UiEvent.ShowToast("SSID cannot be empty.")) - _isHotspotEnabled.value = false - _isProcessing.value = false - return@launch - } - - if (passwordTrimmed.length !in 8..63) { - updateLog("Error: The length of a passphrase must be between 8 and 63.") - _eventFlow.emit(UiEvent.ShowToast("Password must be between 8 and 63 characters.")) - _isHotspotEnabled.value = false - _isProcessing.value = false - return@launch - } - - val ssid = "DIRECT-hs-$ssidTrimmed" - - val band = when (selectedBand) { - "2.4GHz" -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ - "5GHz" -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ - else -> WifiP2pConfig.GROUP_OWNER_BAND_AUTO - } - - val config = WifiP2pConfig.Builder() - .setNetworkName(ssid) - .setPassphrase(passwordTrimmed) - .enablePersistentMode(false) - .setGroupOperatingBand(band) - .build() - - try { - wifiManager.createGroup(channel, config, object : WifiP2pManager.ActionListener { - override fun onSuccess() { - viewModelScope.launch { - updateLog("Hotspot started successfully.") - _isHotspotEnabled.value = true - updateLog("------------------- Hotspot Info -------------------") - updateLog("SSID: $ssid") - updateLog("Password: $passwordTrimmed") - val bandStr = when (band) { - WifiP2pConfig.GROUP_OWNER_BAND_2GHZ -> "2.4GHz" - WifiP2pConfig.GROUP_OWNER_BAND_5GHZ -> "5GHz" - else -> "Auto" - } - updateLog("Band: $bandStr") - updateLog("---------------------------------------------------") - _isProcessing.value = false - _eventFlow.emit(UiEvent.ShowToast("Hotspot started successfully.")) - _eventFlow.emit(UiEvent.StartProxyService) - showHotspotStatusNotification() - - } - - startDataUsageTracking() - - - } - - override fun onFailure(reason: Int) { - val reasonStr = when (reason) { - WifiP2pManager.ERROR -> "General error" - WifiP2pManager.P2P_UNSUPPORTED -> "P2P Unsupported" - WifiP2pManager.BUSY -> "System is busy" - else -> "Unknown error" - } - viewModelScope.launch { - updateLog("Failed to start hotspot. Reason: $reasonStr") - _isHotspotEnabled.value = false - _isProcessing.value = false - _eventFlow.emit(UiEvent.ShowToast("Failed to start hotspot: $reasonStr")) - } - } - }) - } catch (e: Exception) { - viewModelScope.launch { - updateLog("Exception: ${e.message}") - _isHotspotEnabled.value = false - _isProcessing.value = false - _eventFlow.emit(UiEvent.ShowToast("Exception occurred: ${e.message}")) - } - } - } - } - - // ----- Function to Stop the Hotspot ----- - fun onButtonStopTapped() { - viewModelScope.launch { - if (!_isHotspotEnabled.value) { - updateLog("Error: Hotspot is not enabled.") - _eventFlow.emit(UiEvent.ShowToast("Hotspot is not enabled.")) - return@launch - } - - _isProcessing.value = true // Start processing - - try { - wifiManager.removeGroup(channel, object : WifiP2pManager.ActionListener { - override fun onSuccess() { - viewModelScope.launch { - updateLog("Hotspot stopped successfully.") - _isHotspotEnabled.value = false - _connectedDevices.value = emptyList() - _isProcessing.value = false - _eventFlow.emit(UiEvent.ShowToast("Hotspot stopped successfully.")) - _eventFlow.emit(UiEvent.StopProxyService) - removeHotspotStatusNotification() - - } -// saveSessionDataUsage() - } - - override fun onFailure(reason: Int) { - val reasonStr = when (reason) { - WifiP2pManager.ERROR -> "General error" - WifiP2pManager.P2P_UNSUPPORTED -> "P2P Unsupported" - WifiP2pManager.BUSY -> "System is busy" - else -> "Unknown error" - } - viewModelScope.launch { - updateLog("Failed to stop hotspot. Reason: $reasonStr") - _isHotspotEnabled.value = true // Assuming it was enabled - _isProcessing.value = false - _eventFlow.emit(UiEvent.ShowToast("Failed to stop hotspot: $reasonStr")) - } - } - }) - } catch (e: Exception) { - viewModelScope.launch { - updateLog("Exception: ${e.message}") - _isHotspotEnabled.value = true // Assuming it was enabled - _isProcessing.value = false - _eventFlow.emit(UiEvent.ShowToast("Exception occurred: ${e.message}")) - } - } - } - } - - - fun updatePasswordVisibility(isVisible: Boolean) { - passwordVisible = isVisible - } - //*****************************************************Fine**************************************** - -// fun updateDataUsageThreshold(threshold: Long) { -// _dataUsageThreshold.value = threshold -// // Save to DataStore -// viewModelScope.launch { -// dataStore.edit { preferences -> -// preferences[DATA_USAGE_THRESHOLD_KEY] = threshold -// } -// } -// } - - -// In the coroutine where you update uploadSpeed and downloadSpeed -// viewModelScope.launch { -// var time = 0f -// while (true) { -// delay(1000) -// // Existing code to update speeds -// -// // Update entries -// _uploadSpeedEntries.value = _uploadSpeedEntries.value + Entry(time, uploadSpeedKbps.toFloat()) -// _downloadSpeedEntries.value = _downloadSpeedEntries.value + Entry(time, downloadSpeedKbps.toFloat()) -// time += 1f -// -// // Limit the number of entries to, e.g., 60 -// if (_uploadSpeedEntries.value.size > 60) { -// _uploadSpeedEntries.value = _uploadSpeedEntries.value.drop(1) -// _downloadSpeedEntries.value = _downloadSpeedEntries.value.drop(1) -// } -//// if (sessionRxBytes + sessionTxBytes >= dataUsageThreshold.value && !thresholdReached) { -//// thresholdReached = true -//// sendDataUsageNotification() -//// } -// } -// } - - -// fun saveSessionDataUsage() { -// val (rxBytes, txBytes) = getSessionDataUsage() -// val today = LocalDate.now() -// val existingRecord = _historicalDataUsage.value.find { it.date == today } -// val updatedRecord = existingRecord?.copy( -// rxBytes = existingRecord.rxBytes + rxBytes, -// txBytes = existingRecord.txBytes + txBytes -// ) -// ?: DataUsageRecord(date = today, rxBytes = rxBytes, txBytes = txBytes) -// val updatedList = _historicalDataUsage.value.filterNot { it.date == today } + updatedRecord -// _historicalDataUsage.value = updatedList - - // Save to DataStore -// val json = Json.encodeToString(updatedList) -// viewModelScope.launch { -// dataStore.edit { preferences -> -// preferences[DATA_USAGE_KEY] = json -// } -// } -// } -// -// fun sendDataUsageNotification() { -// val notificationManager = getApplication().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager -// val notificationId = 1 -// -// val notification = NotificationCompat.Builder(getApplication(), "data_usage_channel") -// .setSmallIcon(R.drawable.ic_notification) -// .setContentTitle("Data Usage Alert") -// .setContentText("You have reached your data usage threshold.") -// .setPriority(NotificationCompat.PRIORITY_HIGH) -// .build() -// -// notificationManager.notify(notificationId, notification) -// } - fun contactSupport() { - val intent = Intent(Intent.ACTION_SENDTO).apply { - data = Uri.parse("mailto:") - putExtra(Intent.EXTRA_EMAIL, arrayOf("2024@example.com")) - putExtra(Intent.EXTRA_SUBJECT, "Support Request") - } - intent.resolveActivity(getApplication().packageManager)?.let { - getApplication().startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) - } ?: run { - viewModelScope.launch { - _eventFlow.emit(UiEvent.ShowToast("No email client available")) - } - } - } - - - fun submitFeedback(feedback: String) { - // Send feedback via email or to a server - // For example, open an email intent - val intent = Intent(Intent.ACTION_SENDTO).apply { - data = Uri.parse("mailto:") - putExtra(Intent.EXTRA_EMAIL, arrayOf("support@example.com")) - putExtra(Intent.EXTRA_SUBJECT, "App Feedback") - putExtra(Intent.EXTRA_TEXT, feedback) - } - intent.resolveActivity(getApplication().packageManager)?.let { - getApplication().startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) - } ?: run { - // Handle the case where no email client is available - viewModelScope.launch { - _eventFlow.emit(UiEvent.ShowToast("No email client available")) - } - } - } - - fun sendDeviceConnectionNotification(device: WifiP2pDevice, connected: Boolean) { - val notificationManager = - getApplication().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val notificationId = device.deviceAddress.hashCode() - - val contentText = if (connected) { - "Device connected: ${device.deviceName}" - } else { - "Device disconnected: ${device.deviceName}" - } - - val notification = - NotificationCompat.Builder(getApplication(), "device_connection_channel") - .setSmallIcon(R.drawable.ic_notification) - .setContentTitle("Device Connection") - .setContentText(contentText) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .build() - - notificationManager.notify(notificationId, notification) - } - - fun showHotspotStatusNotification() { - val notificationManager = - getApplication().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val notificationId = 1000 // Unique ID for the hotspot status notification - - val notification = NotificationCompat.Builder(getApplication(), "tether_guard") - .setSmallIcon(R.drawable.ic_notification) - .setContentTitle("Title") - .setContentText("Content") - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setDefaults(NotificationCompat.DEFAULT_ALL) - .apply { - if (!notificationSoundEnabled.value) { - setSound(null) - } - if (!notificationVibrationEnabled.value) { - setVibrate(null) - } - } - .build() - } - - fun removeHotspotStatusNotification() { - val notificationManager = - getApplication().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val notificationId = 1000 - notificationManager.cancel(notificationId) - } - - fun scheduleHotspotStart(timeInMillis: Long) { - val delay = timeInMillis - System.currentTimeMillis() - val workRequest = OneTimeWorkRequestBuilder() - .setInitialDelay(delay, TimeUnit.MILLISECONDS) - .build() - WorkManager.getInstance(getApplication()).enqueue(workRequest) - } - - fun scheduleHotspotStop(timeInMillis: Long) { - val delay = timeInMillis - System.currentTimeMillis() - val workRequest = OneTimeWorkRequestBuilder() - .setInitialDelay(delay, TimeUnit.MILLISECONDS) - .build() - WorkManager.getInstance(getApplication()).enqueue(workRequest) - } - - - fun startDataUsageTracking() { - sessionStartRxBytes = TrafficStats.getTotalRxBytes() - sessionStartTxBytes = TrafficStats.getTotalTxBytes() - } - - fun getSessionDataUsage(): Pair { - val currentRxBytes = TrafficStats.getTotalRxBytes() - val currentTxBytes = TrafficStats.getTotalTxBytes() - val rxBytes = currentRxBytes - sessionStartRxBytes - val txBytes = currentTxBytes - sessionStartTxBytes - return Pair(rxBytes, txBytes) - } - - - // Function to block a device - fun blockDevice(deviceAddress: String) { - // Add to blocked addresses - val updatedSet = _blockedMacAddresses.value + deviceAddress - _blockedMacAddresses.value = updatedSet - - // Save to DataStore - viewModelScope.launch { - dataStore.edit { preferences -> - preferences[BLOCKED_MAC_ADDRESSES_KEY] = updatedSet - } - } - - // Update blocked devices list - updateBlockedDevices() - - // Enforce access control - enforceAccessControl() - } - - - // Function to unblock a device - fun unblockDevice(deviceAddress: String) { - // Remove from blocked addresses - val updatedSet = _blockedMacAddresses.value - deviceAddress - _blockedMacAddresses.value = updatedSet - - // Save to DataStore - viewModelScope.launch { - dataStore.edit { preferences -> - preferences[BLOCKED_MAC_ADDRESSES_KEY] = updatedSet - } - } - - // Update blocked devices list - updateBlockedDevices() - } - - - // Function to save blocked devices to DataStore - private fun saveBlockedDevices() { - val blockedAddresses = _connectedDeviceInfos.value - .filter { it.isBlocked } - .map { it.device.deviceAddress } - .toSet() - - viewModelScope.launch { - dataStore.edit { preferences -> - preferences[BLOCKED_MAC_ADDRESSES_KEY] = blockedAddresses - } - } - } - - // Function to load blocked devices - private fun updateBlockedDevices() { - _blockedDeviceInfos.value = _blockedMacAddresses.value.map { macAddress -> - DeviceInfo( - device = WifiP2pDevice().apply { deviceAddress = macAddress }, - isBlocked = true - ) - } - } - - // Enforce access control based on blocked devices - private fun enforceAccessControl() { - val devicesToDisconnect = _connectedDeviceInfos.value.filter { deviceInfo -> - deviceInfo.isBlocked - } - - devicesToDisconnect.forEach { deviceInfo -> - disconnectDevice(deviceInfo) - } - } - - fun disconnectDevice(deviceInfo: DeviceInfo) { - // Wi-Fi P2P does not provide a direct way to disconnect a single device. - // However, if you are the group owner, you can remove the group to disconnect all devices - // and then reform the group. Alternatively, you can attempt to cancel the connection. - - // For illustration purposes, we'll attempt to remove the group. - wifiManager.removeGroup(channel, object : WifiP2pManager.ActionListener { - override fun onSuccess() { - updateLog("Disconnected device: ${deviceInfo.device.deviceName}") - // Remove the device from the connected devices list - _connectedDeviceInfos.value = _connectedDeviceInfos.value.filterNot { - it.device.deviceAddress == deviceInfo.device.deviceAddress - } - // Optionally, reinitialize the group to allow other devices to reconnect - // This is a workaround due to API limitations -// initializeGroup() - } - - override fun onFailure(reason: Int) { - updateLog("Failed to disconnect device: ${deviceInfo.device.deviceName}") - } - }) - } - -// // Function to initialize the group again after removing it -// private fun initializeGroup() { -// wifiManager.createGroup(channel, object : WifiP2pManager.ActionListener { -// override fun onSuccess() { -// updateLog("Group reinitialized after device disconnection.") -// } -// -// override fun onFailure(reason: Int) { -// updateLog("Failed to reinitialize group after device disconnection.") -// } -// }) -// } - - - // // Update alias function - fun updateDeviceAlias(deviceAddress: String, alias: String) { - // Update aliases map - val updatedAliases = _deviceAliases.value.toMutableMap() - updatedAliases[deviceAddress] = alias - _deviceAliases.value = updatedAliases - - // Save to DataStore - val aliasesJson = Json.encodeToString(updatedAliases) - viewModelScope.launch { - dataStore.edit { preferences -> - preferences[DEVICE_ALIAS_KEY] = aliasesJson - } - } - - // Update connected devices - _connectedDeviceInfos.value = _connectedDeviceInfos.value.map { deviceInfo -> - if (deviceInfo.device.deviceAddress == deviceAddress) { - deviceInfo.copy(alias = alias) - } else { - deviceInfo - } - } - } - - fun updateNotificationEnabled(enabled: Boolean) { - _notificationEnabled.value = enabled - // Save to DataStore - } - - fun updateNotificationSoundEnabled(enabled: Boolean) { - _notificationSoundEnabled.value = enabled - // Save to DataStore - } - - fun updateNotificationVibrationEnabled(enabled: Boolean) { - _notificationVibrationEnabled.value = enabled - // Save to DataStore - } -} diff --git a/app/src/main/java/com/example/wifip2photspot/ImprovedHeader.kt b/app/src/main/java/com/example/wifip2photspot/ImprovedHeader.kt index ff8a117..511b9a6 100644 --- a/app/src/main/java/com/example/wifip2photspot/ImprovedHeader.kt +++ b/app/src/main/java/com/example/wifip2photspot/ImprovedHeader.kt @@ -1,53 +1,48 @@ package com.example.wifip2photspot - - - import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.compose.foundation.layout.padding -//@OptIn(ExperimentalMaterial3Api::class) -//@Composable -//fun ImprovedHeader(isHotspotEnabled: Boolean) { -// var showMenu by remember { mutableStateOf(false) } @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ImprovedHeader(isHotspotEnabled: Boolean) { +fun ImprovedHeader( + onSettingsClick: () -> Unit +) { var showMenu by remember { mutableStateOf(false) } TopAppBar( title = { Text("Asol") }, navigationIcon = { - IconButton(onClick = { /* Open navigation drawer */ }) { + IconButton(onClick = { /* Open navigation drawer if needed */ }) { Icon(Icons.Filled.Menu, contentDescription = "Menu") } }, actions = { - IconButton(onClick = { /* Open settings */ }) { + IconButton(onClick = onSettingsClick) { Icon(Icons.Filled.Settings, contentDescription = "Settings") } IconButton(onClick = { showMenu = !showMenu }) { Icon(Icons.Filled.MoreVert, contentDescription = "More options") } DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { - DropdownMenuItem(text = { Text("Help") }, onClick = { /* Navigate to help */ }) - DropdownMenuItem(text = { Text("About") }, onClick = { /* Navigate to about */ }) + DropdownMenuItem( + text = { Text("Help") }, + onClick = { /* Navigate to help */ } + ) + DropdownMenuItem( + text = { Text("About") }, + onClick = { /* Navigate to about */ } + ) + // Other menu items as needed } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, titleContentColor = MaterialTheme.colorScheme.onPrimary, actionIconContentColor = MaterialTheme.colorScheme.onPrimary - ), -// elevation = 4.dp + ) ) } diff --git a/app/src/main/java/com/example/wifip2photspot/InputFieldsSection.kt b/app/src/main/java/com/example/wifip2photspot/InputFieldsSection.kt index 7db80ce..94e1668 100644 --- a/app/src/main/java/com/example/wifip2photspot/InputFieldsSection.kt +++ b/app/src/main/java/com/example/wifip2photspot/InputFieldsSection.kt @@ -1,21 +1,35 @@ // InputFieldsSection.kt package com.example.wifip2photspot -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material.icons.filled.Wifi +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.focus.* -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.text.input.* +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.wear.compose.material.ContentAlpha @@ -26,11 +40,6 @@ fun InputFieldsSection( passwordInput: TextFieldValue, onPasswordChange: (TextFieldValue) -> Unit, isHotspotEnabled: Boolean, - proxyPort: Int, - onProxyPortChange: (Int) -> Unit, - selectedBand: String, - onBandSelected: (String) -> Unit, - bands: List ) { // State for password visibility var passwordVisible by rememberSaveable { mutableStateOf(false) } @@ -133,80 +142,80 @@ 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) - ) - } - } +// // 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) +// ) +// } } - - // Band Selection Section - BandSelection( - selectedBand = selectedBand, - onBandSelected = onBandSelected, - bands = bands, - isHotspotEnabled = isHotspotEnabled - ) } - } -} -@Composable -fun BandSelection( - selectedBand: String, - onBandSelected: (String) -> Unit, - bands: List, - isHotspotEnabled: Boolean -) { - Column(modifier = Modifier.padding(16.dp)) { - Text( - text = "Select Band", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(bottom = 8.dp) - ) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { - bands.forEach { band -> - OutlinedButton( - onClick = { onBandSelected(band) }, - colors = ButtonDefaults.outlinedButtonColors( - containerColor = if (selectedBand == band) - MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - else - Color.Transparent - ), - enabled = !isHotspotEnabled, - modifier = Modifier - .weight(1f) - .padding(horizontal = 4.dp) - ) { - Text( - text = band, - color = if (selectedBand == band) - MaterialTheme.colorScheme.primary - else - MaterialTheme.colorScheme.onSurface - ) - } - } - } +// // Band Selection Section +// BandSelection( +// selectedBand = selectedBand, +// onBandSelected = onBandSelected, +// bands = bands, +// isHotspotEnabled = isHotspotEnabled +// ) } } - +// +//@Composable +//fun BandSelection( +// selectedBand: String, +// onBandSelected: (String) -> Unit, +// bands: List, +// isHotspotEnabled: Boolean +//) { +// Column(modifier = Modifier.padding(16.dp)) { +// Text( +// text = "Select Band", +// style = MaterialTheme.typography.titleMedium, +// modifier = Modifier.padding(bottom = 8.dp) +// ) +// Row( +// horizontalArrangement = Arrangement.SpaceBetween, +// modifier = Modifier.fillMaxWidth() +// ) { +// bands.forEach { band -> +// OutlinedButton( +// onClick = { onBandSelected(band) }, +// colors = ButtonDefaults.outlinedButtonColors( +// containerColor = if (selectedBand == band) +// MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) +// else +// Color.Transparent +// ), +// enabled = !isHotspotEnabled, +// modifier = Modifier +// .weight(1f) +// .padding(horizontal = 4.dp) +// ) { +// Text( +// text = band, +// color = if (selectedBand == band) +// MaterialTheme.colorScheme.primary +// else +// MaterialTheme.colorScheme.onSurface +// ) +// } +// } +// } +// } +//} +// +// diff --git a/app/src/main/java/com/example/wifip2photspot/MACAddressFilterSection.kt b/app/src/main/java/com/example/wifip2photspot/MACAddressFilterSection.kt deleted file mode 100644 index 996d43a..0000000 --- a/app/src/main/java/com/example/wifip2photspot/MACAddressFilterSection.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.example.wifip2photspot - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun MACAddressFilterSection( - allowedMacAddresses: Set, - blockedMacAddresses: Set, - onAddAllowed: (String) -> Unit, - onRemoveAllowed: (String) -> Unit, - onAddBlocked: (String) -> Unit, - onRemoveBlocked: (String) -> Unit -) { - var macInput by remember { mutableStateOf("") } - - Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { - Text("MAC Address Filtering", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - - OutlinedTextField( - value = macInput, - onValueChange = { macInput = it }, - label = { Text("MAC Address") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Row { - Button(onClick = { onAddAllowed(macInput); macInput = "" }) { - Text("Add to Whitelist") - } - Spacer(modifier = Modifier.width(8.dp)) - Button(onClick = { onAddBlocked(macInput); macInput = "" }) { - Text("Add to Blacklist") - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Text("Whitelisted MAC Addresses:") - allowedMacAddresses.forEach { mac -> - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text(mac) - IconButton(onClick = { onRemoveAllowed(mac) }) { - Icon(Icons.Default.Delete, contentDescription = "Remove") - } - } - } - - Spacer(modifier = Modifier.height(8.dp)) - - Text("Blacklisted MAC Addresses:") - blockedMacAddresses.forEach { mac -> - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text(mac) - IconButton(onClick = { onRemoveBlocked(mac) }) { - Icon(Icons.Default.Delete, contentDescription = "Remove") - } - } - } - } -} diff --git a/app/src/main/java/com/example/wifip2photspot/MainActivity.kt b/app/src/main/java/com/example/wifip2photspot/MainActivity.kt index 55c51bf..ca9efed 100644 --- a/app/src/main/java/com/example/wifip2photspot/MainActivity.kt +++ b/app/src/main/java/com/example/wifip2photspot/MainActivity.kt @@ -1,269 +1,223 @@ // MainActivity.kt package com.example.wifip2photspot - import android.Manifest -import android.app.AlertDialog -import android.app.NotificationChannel -import android.app.NotificationManager +import android.app.Activity.WIFI_P2P_SERVICE import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager +import android.location.LocationManager +import android.net.Uri +import android.net.wifi.WifiManager import android.net.wifi.p2p.WifiP2pManager import android.os.Build import android.os.Bundle -import android.widget.Toast +import android.provider.Settings import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.annotation.RequiresApi +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.compose.foundation.layout.fillMaxSize -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import androidx.datastore.preferences.preferencesDataStore -import androidx.lifecycle.ViewModelProvider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.contentcapture.ContentCaptureManager.Companion.isEnabled -import com.example.wifip2photspot.Proxy.ProxyService -import com.example.wifip2photspot.Proxy.ProxyService.Companion.CHANNEL_ID +import androidx.core.content.ContextCompat +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import androidx.lifecycle.ViewModelProvider +import com.example.wifip2photspot.ui.screens.PermissionDeniedDialog +import com.example.wifip2photspot.ui.screens.ServiceDisabledDialog import com.example.wifip2photspot.ui.theme.WiFiP2PHotspotTheme - -private val Context.dataStore by preferencesDataStore(name = "settings") +import com.example.wifip2photspot.viewModel.HotspotViewModel +import com.example.wifip2photspot.viewModel.HotspotViewModelFactory +import com.example.wifip2photspot.viewModel.WifiDirectBroadcastReceiver class MainActivity : ComponentActivity() { - - private lateinit var viewModel: HotspotViewModel + private lateinit var hotspotViewModel: HotspotViewModel + private val Context.dataStore: DataStore by preferencesDataStore(name = "settings") + private lateinit var wifiP2pManager: WifiP2pManager + private lateinit var channel: WifiP2pManager.Channel private lateinit var receiver: WifiDirectBroadcastReceiver - private lateinit var intentFilter: IntentFilter - - private val channelName = "Data Usage Alerts" - private val channelDescription = "Notifications for data usage thresholds" - private val importance = NotificationManager.IMPORTANCE_HIGH - - @OptIn(ExperimentalComposeUiApi::class) - @RequiresApi(Build.VERSION_CODES.Q) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // Initialize DataStore - val dataStore = applicationContext.dataStore - - // Initialize ViewModel with Factory - viewModel = ViewModelProvider( - this, - HotspotViewModelFactory(application, dataStore) - )[HotspotViewModel::class.java] - - // Initialize and Register BroadcastReceiver - receiver = WifiDirectBroadcastReceiver( - manager = viewModel.wifiManager, - channel = viewModel.channel, - viewModel = viewModel - ) -// intentFilter = IntentFilter().apply { -// addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION) -// addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION) -// // Add other actions if necessary -// } - intentFilter = IntentFilter().apply { - addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION) - addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION) - addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION) - addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel(CHANNEL_ID, channelName, importance).apply { - description = channelDescription + private var receiverRegistered = false + + private val permissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + val deniedPermissions = permissions.filter { !it.value }.keys + if (deniedPermissions.isEmpty()) { + initializeWifiP2p() + } else { + showPermissionRationaleDialog(deniedPermissions.toList()) } - // Register the channel with the system - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(channel) } + private var showPermissionDialog by mutableStateOf(false) + private var deniedPermissionsList by mutableStateOf>(emptyList()) + private var showWifiDisabledDialog by mutableStateOf(false) + private var showLocationDisabledDialog by mutableStateOf(false) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) -// // Request necessary permissions -// if (!allPermissionsGranted()) { -// requestPermissions() -// } + hotspotViewModel = HotspotViewModel(applicationContext, dataStore) setContent { - // Theme State - var isDarkTheme by rememberSaveable { mutableStateOf(false) } + val isDarkTheme by hotspotViewModel.isDarkTheme.collectAsState() WiFiP2PHotspotTheme(useDarkTheme = isDarkTheme) { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - PermissionHandler { - WiFiP2PHotspotApp( - viewModel = viewModel, - activity = this, - isDarkTheme = isDarkTheme, - onThemeChange = { isDark -> - isDarkTheme = isDark - }) - // Start or stop the proxy based on the hotspot state - if (isEnabled) { - startProxyService() - } else { - stopProxyService() + WiFiP2PHotspotApp(hotspotViewModel = hotspotViewModel) + + LaunchedEffect(Unit) { + if (checkAndRequestPermissions()) { + initializeWifiP2p() } } + + // Show dialogs for Wi-Fi, Location, and permissions + if (showWifiDisabledDialog) { + ServiceDisabledDialog( + serviceName = "Wi-Fi", + onDismiss = { showWifiDisabledDialog = false }, + onConfirm = { openWifiSettings() } + ) + } + + if (showLocationDisabledDialog) { + ServiceDisabledDialog( + serviceName = "Location", + onDismiss = { showLocationDisabledDialog = false }, + onConfirm = { openLocationSettings() } + ) + } + + if (showPermissionDialog) { + PermissionDeniedDialog( + deniedPermissions = deniedPermissionsList, + onDismiss = { showPermissionDialog = false }, + onConfirm = { + showPermissionDialog = false + requestPermissions(deniedPermissionsList) + }, + onOpenSettings = { + val intent = Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", packageName, null) + ) + startActivity(intent) + showPermissionDialog = false + } + ) + } } } } } -// // In your Application class or MainActivity -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { -// val name = "Data Usage Alerts" -// val descriptionText = "Notifications for data usage thresholds" -// val importance = NotificationManager.IMPORTANCE_HIGH -// val channel = NotificationChannel("data_usage_channel", name, importance).apply { -// description = descriptionText -// } -// val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager -// notificationManager.createNotificationChannel(channel) -// } - private fun startProxyService() { - val intent = Intent(this, ProxyService::class.java) - startService(intent) + override fun onResume() { + super.onResume() + if (!isWifiEnabled()) { + showWifiDisabledDialog = true + } else if (!isLocationEnabled()) { + showLocationDisabledDialog = true + } else { + if (checkAndRequestPermissions()) { + initializeWifiP2p() + } + } + + if (this::wifiP2pManager.isInitialized && this::channel.isInitialized) { + val intentFilter = IntentFilter().apply { + addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION) + addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION) + } + receiver = WifiDirectBroadcastReceiver(wifiP2pManager, channel, hotspotViewModel) + registerReceiver(receiver, intentFilter) + receiverRegistered = true + } + } + + override fun onPause() { + super.onPause() + if (receiverRegistered) { + unregisterReceiver(receiver) + receiverRegistered = false + } } - private fun stopProxyService() { - val intent = Intent(this, ProxyService::class.java) - stopService(intent) + private fun checkAndRequestPermissions(): Boolean { + val requiredPermissions = mutableListOf() + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + requiredPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.NEARBY_WIFI_DEVICES) != PackageManager.PERMISSION_GRANTED) { + requiredPermissions.add(Manifest.permission.NEARBY_WIFI_DEVICES) + } + } + + return if (requiredPermissions.isNotEmpty()) { + requestPermissions(requiredPermissions) + false + } else { + true + } } - override fun onDestroy() { - super.onDestroy() - // Ensure services are stopped + private fun requestPermissions(permissions: List) { + permissionLauncher.launch(permissions.toTypedArray()) } - override fun onResume() { - super.onResume() - registerReceiver(receiver, intentFilter) + private fun showPermissionRationaleDialog(deniedPermissions: List) { + deniedPermissionsList = deniedPermissions + showPermissionDialog = true } - override fun onPause() { - super.onPause() - unregisterReceiver(receiver) + private fun initializeWifiP2p() { + if (!isWifiEnabled()) { + showWifiDisabledDialog = true + return + } + + if (!isLocationEnabled()) { + showLocationDisabledDialog = true + return + } + + wifiP2pManager = getSystemService(WIFI_P2P_SERVICE) as WifiP2pManager + channel = wifiP2pManager.initialize(this, mainLooper, null) +// hotspotViewModel.initialize(wifiP2pManager, channel) } -} + private fun isWifiEnabled(): Boolean { + val wifiManager = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager + return wifiManager.isWifiEnabled + } + + private fun isLocationEnabled(): Boolean { + val locationManager = getSystemService(LOCATION_SERVICE) as LocationManager + return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || + locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } -// private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { -// ContextCompat.checkSelfPermission( -// baseContext, it -// ) == PackageManager.PERMISSION_GRANTED -// } -// -// private fun requestPermissions() { -// // Check if we should show a rationale -// val shouldShowRationale = REQUIRED_PERMISSIONS.any { permission -> -// ActivityCompat.shouldShowRequestPermissionRationale(this, permission) -// } -// -// if (shouldShowRationale) { -// // Show a dialog explaining why the permissions are needed -// AlertDialog.Builder(this) -// .setTitle("Permissions Required") -// .setMessage("This app requires location and Wi-Fi permissions to function correctly.") -// .setPositiveButton("OK") { dialog, _ -> -// ActivityCompat.requestPermissions( -// this, -// REQUIRED_PERMISSIONS, -// PERMISSION_REQUEST_CODE -// ) -// dialog.dismiss() -// } -// .setNegativeButton("Cancel") { dialog, _ -> -// Toast.makeText( -// this, -// "Permissions not granted by the user.", -// Toast.LENGTH_SHORT -// ).show() -// dialog.dismiss() -// finish() -// } -// .create() -// .show() -// } else { -// // Directly request permissions -// ActivityCompat.requestPermissions( -// this, -// REQUIRED_PERMISSIONS, -// PERMISSION_REQUEST_CODE -// ) -// } -// } -// -// companion object { -// private const val PERMISSION_REQUEST_CODE = 1 -// private val REQUIRED_PERMISSIONS = arrayOf( -// Manifest.permission.ACCESS_FINE_LOCATION, -// Manifest.permission.CHANGE_WIFI_STATE, -// Manifest.permission.ACCESS_WIFI_STATE, -// Manifest.permission.INTERNET, -// Manifest.permission.ACCESS_NETWORK_STATE, -// // Add NEARBY_WIFI_DEVICES if targeting Android 13+ -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { -// Manifest.permission.NEARBY_WIFI_DEVICES -// } else { -// null -// } -// ).filterNotNull().toTypedArray() -// } -// -// override fun onRequestPermissionsResult( -// requestCode: Int, -// permissions: Array, -// grantResults: IntArray -// ) { -// if (requestCode == PERMISSION_REQUEST_CODE) { -// if (allPermissionsGranted()) { -// // Permissions granted, proceed as normal -// Toast.makeText( -// this, -// "Permissions granted.", -// Toast.LENGTH_SHORT -// ).show() -// } else { -// // Permissions denied -// Toast.makeText( -// this, -// "Permissions not granted by the user.", -// Toast.LENGTH_SHORT -// ).show() -// // Optionally, direct the user to app settings -// AlertDialog.Builder(this) -// .setTitle("Permissions Denied") -// .setMessage("You have denied some permissions. Allow all permissions at [Settings] > [Permissions]") -// .setPositiveButton("Open Settings") { dialog, _ -> -// val intent = -// android.content.Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS) -// val uri: android.net.Uri = -// android.net.Uri.fromParts("package", packageName, null) -// intent.data = uri -// startActivity(intent) -// dialog.dismiss() -// } -// .setNegativeButton("Exit") { dialog, _ -> -// dialog.dismiss() -// finish() -// } -// .create() -// .show() -// } -// } -// super.onRequestPermissionsResult(requestCode, permissions, grantResults) -// } -//} + private fun openWifiSettings() { + val intent = Intent(Settings.ACTION_WIFI_SETTINGS) + startActivity(intent) + showWifiDisabledDialog = false + } + + private fun openLocationSettings() { + val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + startActivity(intent) + showLocationDisabledDialog = false + } +} diff --git a/app/src/main/java/com/example/wifip2photspot/NotificationSettingsSection.kt b/app/src/main/java/com/example/wifip2photspot/NotificationSettingsSection.kt deleted file mode 100644 index 356c4ef..0000000 --- a/app/src/main/java/com/example/wifip2photspot/NotificationSettingsSection.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.wifip2photspot - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.focus.* -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.text.input.* -import androidx.compose.ui.unit.dp -import androidx.wear.compose.material.ContentAlpha - -@Composable -fun NotificationSettingsSection( - notificationEnabled: Boolean, - onNotificationEnabledChange: (Boolean) -> Unit, - soundEnabled: Boolean, - onSoundEnabledChange: (Boolean) -> Unit, - vibrationEnabled: Boolean, - onVibrationEnabledChange: (Boolean) -> Unit -) { - Column(modifier = Modifier.padding(16.dp)) { - Text("Notification Settings", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - - SwitchPreference( - label = "Enable Notifications", - checked = notificationEnabled, - onCheckedChange = onNotificationEnabledChange - ) - - SwitchPreference( - label = "Sound", - checked = soundEnabled, - onCheckedChange = onSoundEnabledChange, - enabled = notificationEnabled - ) - - SwitchPreference( - label = "Vibration", - checked = vibrationEnabled, - onCheckedChange = onVibrationEnabledChange, - enabled = notificationEnabled - ) - } -} - -@Composable -fun SwitchPreference( - label: String, - checked: Boolean, - onCheckedChange: (Boolean) -> Unit, - enabled: Boolean = true -) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Text(label, modifier = Modifier.weight(1f)) - Switch( - checked = checked, - onCheckedChange = onCheckedChange, - enabled = enabled - ) - } -} diff --git a/app/src/main/java/com/example/wifip2photspot/PermissionHandler.kt b/app/src/main/java/com/example/wifip2photspot/PermissionHandler.kt deleted file mode 100644 index 7cac5ab..0000000 --- a/app/src/main/java/com/example/wifip2photspot/PermissionHandler.kt +++ /dev/null @@ -1,74 +0,0 @@ -// PermissionHandler.kt -package com.example.wifip2photspot - -import android.Manifest -import android.app.Activity -import android.content.pm.PackageManager -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.platform.LocalContext -import androidx.core.content.ContextCompat - -@Composable -fun PermissionHandler(content: @Composable () -> Unit) { - val context = LocalContext.current - val activity = context as? Activity - var showRationale by remember { mutableStateOf(false) } - - val permissions = arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.CHANGE_WIFI_STATE, - Manifest.permission.ACCESS_WIFI_STATE, - Manifest.permission.INTERNET, - Manifest.permission.ACCESS_NETWORK_STATE, - Manifest.permission.NEARBY_WIFI_DEVICES, - Manifest.permission.CHANGE_NETWORK_STATE, - Manifest.permission.FOREGROUND_SERVICE, -// Manifest.permission.VIBRATE, -// Manifest.permission.BIND_VPN_SERVICE - ) - - val launcher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestMultiplePermissions(), - onResult = { permissionsGranted -> - val allGranted = permissionsGranted.all { it.value } - if (!allGranted) { - showRationale = true - } - } - ) - - LaunchedEffect(key1 = true) { - val allGranted = permissions.all { - ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED - } - if (!allGranted) { - launcher.launch(permissions) - } - } - - if (showRationale) { - // Show a dialog or UI to explain why permissions are needed - AlertDialog( - onDismissRequest = {}, - title = { Text("Permissions Required") }, - text = { Text("This app requires location and Wi-Fi permissions to function correctly.") }, - confirmButton = { - Button(onClick = { launcher.launch(permissions) }) { - Text("Grant Permissions") - } - }, - dismissButton = { - Button(onClick = { /* Handle denial */ }) { - Text("Cancel") - } - } - ) - } else { - content() - } -} diff --git a/app/src/main/java/com/example/wifip2photspot/Proxy/DataUsageSection.kt b/app/src/main/java/com/example/wifip2photspot/Proxy/DataUsageSection.kt deleted file mode 100644 index 9db1054..0000000 --- a/app/src/main/java/com/example/wifip2photspot/Proxy/DataUsageSection.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.example.wifip2photspot.Proxy - -// DataUsageSection.kt -import android.annotation.SuppressLint -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun DataUsageSection( - upload: Int, - download: Int -) { - Card( - elevation = CardDefaults.cardElevation(4.dp), - shape = MaterialTheme.shapes.medium, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text("Data Usage", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - Text("Upload: ${formatBytes(upload)}", style = MaterialTheme.typography.bodyMedium) - Text("Download: ${formatBytes(download)}", style = MaterialTheme.typography.bodyMedium) - } - } -} - -// Utility function to format bytes -@SuppressLint("DefaultLocale") -fun formatBytes(bytes: Int): String { - if (bytes < 1024) return "$bytes B" - val exp = (Math.log(bytes.toDouble()) / Math.log(1024.0)).toInt() - val pre = "KMGTPE"[exp - 1].toString() + "i" - return String.format("%.1f %sB", bytes / Math.pow(1024.0, exp.toDouble()), pre) -} diff --git a/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyControlSection.kt b/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyControlSection.kt deleted file mode 100644 index d5f3380..0000000 --- a/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyControlSection.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.example.wifip2photspot.Proxy - -// ProxyControlSection.kt - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -// ProxyControlSection.kt -@Composable -fun ProxyControlSection( - isProxyRunning: Boolean, - isProcessing: Boolean, - onStartProxy: () -> Unit, - onStopProxy: () -> Unit, - onChangeProxyPort: (Int) -> Unit, - proxyPort: Int -) { - Card( - elevation = CardDefaults.cardElevation(4.dp), - shape = MaterialTheme.shapes.medium, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text("Proxy Server Controls", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - Button( - onClick = onStartProxy, - enabled = !isProxyRunning && !isProcessing - ) { - if (isProcessing && !isProxyRunning) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp - ) - } else { - Text("Start Proxy") - } - } - - Button( - onClick = onStopProxy, - enabled = isProxyRunning && !isProcessing - ) { - if (isProcessing && isProxyRunning) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp - ) - } else { - Text("Stop Proxy") - } - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Optionally, allow users to change the proxy port - 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 { - onChangeProxyPort(it) - } - }, - label = { Text("Port") }, - singleLine = true, - modifier = Modifier.width(100.dp) - ) - } - } - } -} - diff --git a/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyInstructionsSection.kt b/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyInstructionsSection.kt deleted file mode 100644 index bdeb2bd..0000000 --- a/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyInstructionsSection.kt +++ /dev/null @@ -1,179 +0,0 @@ -// ProxyInstructionsSection.kt -package com.example.wifip2photspot.Proxy - -import androidx.compose.foundation.layout.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Info -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material.icons.filled.Wifi -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp - -@Composable -fun ProxyInstructionsSection( - proxyIP: String, - proxyPort: Int -) { - Card( - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), - shape = MaterialTheme.shapes.medium, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - // Section Title with Icon - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Settings, - contentDescription = "Instructions Icon", - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "Configure Connected Devices", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - Divider(color = Color.Gray, thickness = 1.dp) - - Spacer(modifier = Modifier.height(12.dp)) - - // Instructions List - Column(modifier = Modifier.fillMaxWidth()) { - InstructionStep( - stepNumber = 1, - icon = Icons.Default.Wifi, - description = "Go to Wi-Fi settings on your device." - ) - InstructionStep( - stepNumber = 2, - icon = Icons.Default.Wifi, - description = "Long-press the connected hotspot network and select 'Modify network'." - ) - InstructionStep( - stepNumber = 3, - icon = Icons.Default.Wifi, - description = "Expand 'Advanced options'." - ) - InstructionStep( - stepNumber = 4, - icon = Icons.Default.Wifi, - description = "Under 'Proxy', select 'Manual'." - ) - InstructionStep( - stepNumber = 5, - icon = Icons.Default.Info, - description = "Enter the Proxy hostname and Proxy port as below:" - ) - } - - Spacer(modifier = Modifier.height(12.dp)) - // Display Proxy Configuration - ProxyConfiguration(proxyIP = proxyIP, proxyPort = proxyPort) - - Spacer(modifier = Modifier.height(12.dp)) - Divider(color = Color.Gray, thickness = 1.dp) - - Spacer(modifier = Modifier.height(12.dp)) - // Additional Notes - Text( - text = "Note:", - style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold), - color = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "• Only HTTP and HTTPS traffic is supported.", - style = MaterialTheme.typography.bodyMedium - ) - Text( - text = "• Manual proxy configuration is required on each connected device.", - style = MaterialTheme.typography.bodyMedium - ) - Text( - text = "• Ensure the proxy port is not blocked by any firewall or security settings.", - style = MaterialTheme.typography.bodyMedium - ) - } - } -} - -@Composable -fun InstructionStep( - stepNumber: Int, - icon: androidx.compose.ui.graphics.vector.ImageVector, - description: String -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(vertical = 4.dp) - ) { - Text( - text = "$stepNumber.", - style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold), - modifier = Modifier.width(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Icon( - imageVector = icon, - contentDescription = "Step $stepNumber Icon", - tint = MaterialTheme.colorScheme.secondary, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = description, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.fillMaxWidth() - ) - } -} - -@Composable -fun ProxyConfiguration(proxyIP: String, proxyPort: Int) { - Column(modifier = Modifier.fillMaxWidth()) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Proxy Hostname:", - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.SemiBold - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = proxyIP, - style = MaterialTheme.typography.bodyMedium, - color = Color(0xFF1E88E5) // Blue color for emphasis - ) - } - Spacer(modifier = Modifier.height(4.dp)) - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Proxy Port:", - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.SemiBold - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = proxyPort.toString(), - style = MaterialTheme.typography.bodyMedium, - color = Color(0xFF1E88E5) // Blue color for emphasis - ) - } - } -} diff --git a/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyLimitationsSection.kt b/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyLimitationsSection.kt deleted file mode 100644 index c254ebb..0000000 --- a/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyLimitationsSection.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.wifip2photspot.Proxy - -// ProxyLimitationsSection.kt - - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun ProxyLimitationsSection() { - Card( - elevation = CardDefaults.cardElevation(4.dp), - shape = MaterialTheme.shapes.medium, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text("Proxy Limitations", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - Text( - "• Only HTTP and HTTPS traffic is supported.", - style = MaterialTheme.typography.bodySmall - ) - Text( - "• Other protocols may not function correctly.", - style = MaterialTheme.typography.bodySmall - ) - Text( - "• Manual proxy configuration is required on connected devices.", - style = MaterialTheme.typography.bodySmall - ) - } - } -} diff --git a/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyService.kt b/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyService.kt deleted file mode 100644 index 8e8b9b4..0000000 --- a/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyService.kt +++ /dev/null @@ -1,457 +0,0 @@ -// File: app/src/main/java/com/example/vpnshare/service/ProxyService.kt - -package com.example.wifip2photspot.Proxy - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import android.content.Intent -import android.net.VpnService -import android.os.ParcelFileDescriptor -import androidx.core.app.NotificationCompat -import com.example.wifip2photspot.R -import kotlinx.coroutines.* -import timber.log.Timber -import java.io.* -import java.net.InetSocketAddress -import java.net.ServerSocket -import java.net.Socket -import javax.net.ssl.SSLSocket -import javax.net.ssl.SSLSocketFactory -import java.security.cert.CertificateFactory -import java.security.KeyStore -import javax.net.ssl.TrustManagerFactory -import javax.net.ssl.SSLContext - -enum class ConnectionStatus { - CONNECTED, - CONNECTING, - DISCONNECTED, - ERROR -} - -class ProxyService : VpnService() { - - private var vpnInterface: ParcelFileDescriptor? = null - private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - - companion object { - const val CHANNEL_ID = "ProxyServiceChannel" - const val NOTIFICATION_ID = 1 - const val PROXY_SERVER_ADDRESS = "your.proxy.server.address" // Replace with your proxy server address - const val PROXY_SERVER_PORT = 443 // Typically SSL port - const val SOCKS_PROXY_PORT = 1080 // Port for SOCKS proxy - } - - private var socksProxyJob: Job? = null - - override fun onCreate() { - super.onCreate() - createNotificationChannel() - startForeground(NOTIFICATION_ID, buildNotification()) - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - // Start the foreground service here - startForeground(NOTIFICATION_ID, buildNotification()) - - serviceScope.launch { - try { - setupVpn() - manageVpnTraffic() - startSocksProxy() - } catch (e: Exception) { - Timber.e(e, "Error in ProxyService") - stopSelf() - } - } - return START_STICKY - } - - override fun onDestroy() { - super.onDestroy() - vpnInterface?.close() - vpnInterface = null - serviceScope.cancel() - stopSocksProxy() - } - - /** - * Sets up the VPN interface with specified IP and routing. - */ - private fun setupVpn() { - val builder = Builder() - builder.setSession("VPNShare") - .addAddress("10.0.0.2", 24) // VPN interface IP - .addRoute("0.0.0.0", 0) // Route all traffic through VPN - .setMtu(1500) - .establish()?.let { vpnInterface = it } - ?: throw IOException("Failed to establish VPN interface") - Timber.d("VPN interface established") - } - - /** - * Manages the traffic between the VPN interface and the remote proxy server. - */ - private suspend fun manageVpnTraffic() { - vpnInterface?.fileDescriptor?.let { fd -> - val input = FileInputStream(fd).buffered() - val output = FileOutputStream(fd).buffered() - - // Update connection status to CONNECTING - updateConnectionStatus(ConnectionStatus.CONNECTING) - val sslSocket = createSSLSocket(PROXY_SERVER_ADDRESS, PROXY_SERVER_PORT) - ?: throw IOException("Failed to create SSL socket") - // Update connection status to CONNECTED - updateConnectionStatus(ConnectionStatus.CONNECTED) - Timber.d("Connected to Proxy Server at $PROXY_SERVER_ADDRESS:$PROXY_SERVER_PORT") - - val proxyInput = sslSocket.inputStream.buffered() - val proxyOutput = sslSocket.outputStream.buffered() - - // Launch coroutine to read from VPN and write to Proxy - val vpnToProxy = serviceScope.launch { - try { - val buffer = ByteArray(4096) - var bytesRead: Int - while (true) { - bytesRead = input.read(buffer) - if (bytesRead == -1) break - proxyOutput.write(buffer, 0, bytesRead) - proxyOutput.flush() - updateDataUsage(bytesRead.toLong()) - Timber.d("Forwarded $bytesRead bytes to proxy") - } - } catch (e: Exception) { - Timber.e(e, "Error forwarding data to proxy") - updateConnectionStatus(ConnectionStatus.ERROR) - } - } - - // Launch coroutine to read from Proxy and write to VPN - val proxyToVpn = serviceScope.launch { - try { - val buffer = ByteArray(4096) - var bytesRead: Int - while (true) { - bytesRead = proxyInput.read(buffer) - if (bytesRead == -1) break - output.write(buffer, 0, bytesRead) - output.flush() - updateDataUsage(bytesRead.toLong()) - Timber.d("Received $bytesRead bytes from proxy") - } - } catch (e: Exception) { - Timber.e(e, "Error receiving data from proxy") - updateConnectionStatus(ConnectionStatus.ERROR) - } - } - - // Wait for both coroutines to finish - vpnToProxy.join() - proxyToVpn.join() - - // Clean up - sslSocket.close() - updateConnectionStatus(ConnectionStatus.DISCONNECTED) - Timber.d("Proxy connection closed") - } ?: throw IOException("VPN Interface not established") - } - - /** - * Creates an SSL socket connected to the remote proxy server. - */ - private fun createSSLSocket(serverAddress: String, serverPort: Int): SSLSocket? { - return try { - val sslSocketFactory = createPinnedSSLSocketFactory() - ?: throw IOException("Failed to create pinned SSLSocketFactory") - val sslSocket = sslSocketFactory.createSocket() as SSLSocket - sslSocket.connect(InetSocketAddress(serverAddress, serverPort), 10000) // 10-second timeout - sslSocket.startHandshake() - Timber.d("SSL handshake completed with proxy server") - sslSocket - } catch (e: Exception) { - Timber.e(e, "Failed to create SSL socket") - null - } - } - - /** - * Creates an SSL socket factory with a pinned certificate. - */ - private fun createPinnedSSLSocketFactory(): SSLSocketFactory? { - return try { - // Load the trusted certificate from raw resources - val cf = CertificateFactory.getInstance("X.509") - val caInput = resources.openRawResource(R.raw.server_certificate) // Ensure you have this file in res/raw - val ca = cf.generateCertificate(caInput) - caInput.close() - - // Create a KeyStore containing our trusted CAs - val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) - keyStore.load(null, null) - keyStore.setCertificateEntry("ca", ca) - - // Create a TrustManager that trusts the CAs in our KeyStore - val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) - tmf.init(keyStore) - - // Create an SSLContext that uses our TrustManager - val sslContext = SSLContext.getInstance("TLS") - sslContext.init(null, tmf.trustManagers, null) - - sslContext.socketFactory - } catch (e: Exception) { - Timber.e(e, "Failed to create pinned SSLSocketFactory") - null - } - } - - /** - * Builds the notification for the foreground service. - */ - private fun buildNotification(): Notification { - return NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("VPNShare Proxy Service") - .setContentText("Proxy is running") - .setSmallIcon(R.drawable.ic_proxy) // Ensure you have this icon in your resources - .setPriority(NotificationCompat.PRIORITY_LOW) - .setOngoing(true) // Makes the notification non-dismissible - .build() - } - - /** - * Creates a notification channel for the service. - */ - private fun createNotificationChannel() { - val serviceChannel = NotificationChannel( - CHANNEL_ID, - "VPN Proxy Service Channel", - NotificationManager.IMPORTANCE_LOW - ).apply { - description = "Channel for VPN Proxy Service" - } - val manager = getSystemService(NotificationManager::class.java) - manager.createNotificationChannel(serviceChannel) - } - - - /** - * Updates the data usage in SharedPreferences. - */ - private fun updateDataUsage(bytes: Long) { - val sharedPreferences = getSharedPreferences("Proxy_Settings", Context.MODE_PRIVATE) - val currentUsage = sharedPreferences.getString("dataUsage", "0 MB") ?: "0 MB" - val currentBytes = parseDataUsage(currentUsage) - val totalBytes = currentBytes + bytes - val formattedUsage = formatBytes(totalBytes) - sharedPreferences.edit().putString("dataUsage", formattedUsage).apply() - } - - /** - * Updates the connection status in SharedPreferences. - */ - private fun updateConnectionStatus(status: ConnectionStatus) { - val sharedPreferences = getSharedPreferences("Proxy_Settings", Context.MODE_PRIVATE) - sharedPreferences.edit().putString("connectionStatus", status.name).apply() - } - - /** - * Parses data usage string to bytes. - */ - private fun parseDataUsage(dataUsage: String): Long { - return try { - when { - dataUsage.endsWith("GB") -> { - (dataUsage.replace(" GB", "").toDouble() * 1024 * 1024 * 1024).toLong() - } - dataUsage.endsWith("MB") -> { - (dataUsage.replace(" MB", "").toDouble() * 1024 * 1024).toLong() - } - dataUsage.endsWith("KB") -> { - (dataUsage.replace(" KB", "").toDouble() * 1024).toLong() - } - else -> 0L - } - } catch (e: NumberFormatException) { - 0L - } - } - - /** - * Formats bytes to a human-readable string. - */ - private fun formatBytes(bytes: Long): String { - return when { - bytes >= 1024 * 1024 * 1024 -> String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024)) - bytes >= 1024 * 1024 -> String.format("%.2f MB", bytes / (1024.0 * 1024)) - bytes >= 1024 -> String.format("%.2f KB", bytes / 1024.0) - else -> "$bytes B" - } - } - - /** - * Starts the SOCKS proxy server. - */ - private fun startSocksProxy() { - socksProxyJob = serviceScope.launch { - val serverSocket = ServerSocket(SOCKS_PROXY_PORT) - Timber.d("SOCKS Proxy started on port $SOCKS_PROXY_PORT") - try { - while (isActive) { - val clientSocket = serverSocket.accept() - Timber.d("Accepted SOCKS Proxy client: ${clientSocket.inetAddress.hostAddress}") - handleSocksClient(clientSocket) - } - } catch (e: IOException) { - Timber.e(e, "SOCKS Proxy encountered an error") - } finally { - serverSocket.close() - } - } - } - - /** - * Handles incoming SOCKS proxy client connections. - */ - private suspend fun handleSocksClient(clientSocket: Socket) { - serviceScope.launch { - try { - // Implement SOCKS5 handshake and proxying - val input = clientSocket.getInputStream().buffered() - val output = clientSocket.getOutputStream().buffered() - - // Perform SOCKS5 handshake - // 1. Client sends: [SOCKS Version (0x05), Number of Methods] - val version = input.read() - if (version != 0x05) { - clientSocket.close() - return@launch - } - val nMethods = input.read() - val methods = ByteArray(nMethods) - input.read(methods) - - // 2. Server selects NO AUTHENTICATION (0x00) - output.write(byteArrayOf(0x05, 0x00)) - output.flush() - - // 3. Client sends: [SOCKS Version (0x05), CMD, Reserved, ATYP, DST.ADDR, DST.PORT] - val version2 = input.read() - if (version2 != 0x05) { - clientSocket.close() - return@launch - } - val cmd = input.read() - val reserved = input.read() - val atyp = input.read() - - if (cmd != 0x01) { // Only handle CONNECT command - // Reply: Command not supported - output.write(byteArrayOf(0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0)) - output.flush() - clientSocket.close() - return@launch - } - - val dstAddr: String = when (atyp) { - 0x01 -> { // IPv4 - val addrBytes = ByteArray(4) - input.read(addrBytes) - addrBytes.joinToString(".") { (it.toInt() and 0xFF).toString() }.toString() - } - 0x03 -> { // Domain name - val domainLength = input.read() - val domainBytes = ByteArray(domainLength) - input.read(domainBytes) - String(domainBytes) - } - 0x04 -> { // IPv6 - val addrBytes = ByteArray(16) - input.read(addrBytes) - // Convert to IPv6 string representation - java.net.InetAddress.getByAddress(addrBytes).hostAddress - } - else -> { - // Address type not supported - output.write(byteArrayOf(0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0)) - output.flush() - clientSocket.close() - return@launch - } - } - - val dstPort = input.read() shl 8 or input.read() - - // Connect to destination server - val destSocket = Socket() - try { - destSocket.connect(InetSocketAddress(dstAddr, dstPort), 10000) // 10-second timeout - - // Reply: Success - val reply = ByteArray(10) - reply[0] = 0x05 - reply[1] = 0x00 - reply[2] = 0x00 - reply[3] = 0x01 - reply[4] = 0x00 - reply[5] = 0x00 - reply[6] = 0x00 - reply[7] = 0x00 - reply[8] = 0x00 - reply[9] = 0x00 - output.write(reply) - output.flush() - - // Launch bi-directional proxying - val proxy1 = launch { proxyData(input, destSocket.getOutputStream()) } - val proxy2 = launch { proxyData(destSocket.getInputStream(), output) } - - proxy1.join() - proxy2.join() - - } catch (e: Exception) { - Timber.e(e, "Failed to connect to destination server") - // Reply: Connection refused - output.write(byteArrayOf(0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0)) - output.flush() - } finally { - destSocket.close() - clientSocket.close() - } - - } catch (e: Exception) { - Timber.e(e, "Error handling SOCKS client") - clientSocket.close() - } - } - } - - /** - * Proxies data between input and output streams. - */ - private suspend fun proxyData(input: InputStream, output: OutputStream) { - try { - val buffer = ByteArray(4096) - var bytesRead: Int - while (true) { - bytesRead = input.read(buffer) - if (bytesRead == -1) break - output.write(buffer, 0, bytesRead) - output.flush() - } - } catch (e: IOException) { - // Connection closed or error - } - } - - /** - * Stops the SOCKS proxy server. - */ - private fun stopSocksProxy() { - socksProxyJob?.cancel() - socksProxyJob = null - Timber.d("SOCKS Proxy stopped") - } -} diff --git a/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyStatusSection.kt b/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyStatusSection.kt deleted file mode 100644 index e124903..0000000 --- a/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyStatusSection.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.example.wifip2photspot.Proxy - - - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun ProxyStatusSection( - isProxyRunning: Boolean, - proxyIP: String, - proxyPort: Int -) { - Card( - elevation = CardDefaults.cardElevation(4.dp), - shape = MaterialTheme.shapes.medium, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text("Proxy Server Status", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = if (isProxyRunning) "Running" else "Stopped", - color = if (isProxyRunning) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodyLarge - ) - Spacer(modifier = Modifier.height(8.dp)) - if (isProxyRunning) { - Text("Proxy IP: $proxyIP", style = MaterialTheme.typography.bodyMedium) - Text("Proxy Port: $proxyPort", style = MaterialTheme.typography.bodyMedium) - } - } - } -} diff --git a/app/src/main/java/com/example/wifip2photspot/StartHotspotWorker.kt b/app/src/main/java/com/example/wifip2photspot/StartHotspotWorker.kt deleted file mode 100644 index 1b11e19..0000000 --- a/app/src/main/java/com/example/wifip2photspot/StartHotspotWorker.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.wifip2photspot - -import android.content.Context -import androidx.work.CoroutineWorker -import androidx.work.WorkerParameters - -class StartHotspotWorker( - appContext: Context, - workerParams: WorkerParameters -): CoroutineWorker(appContext, workerParams) { - override suspend fun doWork(): Result { - // Obtain an instance of HotspotViewModel or call the necessary functions directly - // Since ViewModel is not accessible here, you may need to refactor the code to allow this - - // Start the hotspot - // For example, using a singleton or a service - - return Result.success() - } -} diff --git a/app/src/main/java/com/example/wifip2photspot/StopHotspotWorker.kt b/app/src/main/java/com/example/wifip2photspot/StopHotspotWorker.kt deleted file mode 100644 index a9d9c0d..0000000 --- a/app/src/main/java/com/example/wifip2photspot/StopHotspotWorker.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.wifip2photspot - -import android.content.Context -import androidx.work.CoroutineWorker -import androidx.work.WorkerParameters - -class StopHotspotWorker( - appContext: Context, - workerParams: WorkerParameters -): CoroutineWorker(appContext, workerParams) { - override suspend fun doWork(): Result { - // Stop the hotspot - return Result.success() - } -} diff --git a/app/src/main/java/com/example/wifip2photspot/VPN/MyVpnService.kt b/app/src/main/java/com/example/wifip2photspot/VPN/MyVpnService.kt deleted file mode 100644 index fae7672..0000000 --- a/app/src/main/java/com/example/wifip2photspot/VPN/MyVpnService.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.wifip2photspot.VPN - - -import android.content.Intent -import android.net.VpnService -import android.os.ParcelFileDescriptor - -class MyVpnService : VpnService() { - - private var vpnInterface: ParcelFileDescriptor? = null - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - establishVpn() - return START_STICKY - } - - private fun establishVpn() { - Builder().apply { - setSession("WiFiP2PHotspot VPN") - addAddress("10.0.0.2", 32) - addRoute("0.0.0.0", 0) - }.establish()?.let { - vpnInterface = it - } - } - - override fun onDestroy() { - super.onDestroy() - vpnInterface?.close() - vpnInterface = null - } -} diff --git a/app/src/main/java/com/example/wifip2photspot/VPN/VpnControlSection.kt b/app/src/main/java/com/example/wifip2photspot/VPN/VpnControlSection.kt deleted file mode 100644 index b942883..0000000 --- a/app/src/main/java/com/example/wifip2photspot/VPN/VpnControlSection.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.example.wifip2photspot.VPN - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -// VpnControlSection.kt -@Composable -fun VpnControlSection( - isVpnRunning: Boolean, - onStartVpn: () -> Unit, - onStopVpn: () -> Unit -) { - Card( - elevation = CardDefaults.cardElevation(4.dp), - shape = MaterialTheme.shapes.medium, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text("VPN Controls", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - Button( - onClick = onStartVpn, - enabled = !isVpnRunning - ) { - Text("Start VPN") - } - - Button( - onClick = onStopVpn, - enabled = isVpnRunning - ) { - Text("Stop VPN") - } - } - } - } -} diff --git a/app/src/main/java/com/example/wifip2photspot/WiFiDirectBroadcastReceiver.kt b/app/src/main/java/com/example/wifip2photspot/WiFiDirectBroadcastReceiver.kt deleted file mode 100644 index 803ca2e..0000000 --- a/app/src/main/java/com/example/wifip2photspot/WiFiDirectBroadcastReceiver.kt +++ /dev/null @@ -1,56 +0,0 @@ -// WifiDirectBroadcastReceiver.kt -package com.example.wifip2photspot - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.net.NetworkInfo -import android.net.wifi.p2p.WifiP2pManager -import android.util.Log - -class WifiDirectBroadcastReceiver( - private val manager: WifiP2pManager, - private val channel: WifiP2pManager.Channel, - private val viewModel: HotspotViewModel -) : BroadcastReceiver() { - - override fun onReceive(context: Context?, intent: Intent?) { - val action = intent?.action - - when (action) { - WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> { - // Check if Wi-Fi P2P is enabled - val state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1) - val isEnabled = state == WifiP2pManager.WIFI_P2P_STATE_ENABLED - viewModel.setWifiP2pEnabled(isEnabled) - viewModel.updateLog("Wi-Fi P2P Enabled: $isEnabled") - } - - WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> { - // Respond to new connection or disconnections - val networkInfo = intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO) - if (networkInfo != null && networkInfo.isConnected) { - // Connected to a P2P network - manager.requestConnectionInfo(channel) { info -> - if (info.groupFormed && info.isGroupOwner) { - viewModel.updateLog("Group Owner: ${info.groupOwnerAddress.hostAddress}") - } - // Request group info to get connected devices - manager.requestGroupInfo(channel) { group -> - if (group != null) { - val deviceList = group.clientList - viewModel.onDevicesChanged(deviceList) - } - } - } - } else { - // Disconnected from a P2P network - viewModel.onDevicesChanged(emptyList()) - viewModel.updateLog("Disconnected from group.") - } - } - - // Handle other actions if necessary - } - } -} diff --git a/app/src/main/java/com/example/wifip2photspot/WiFiP2PHotspotApp.kt b/app/src/main/java/com/example/wifip2photspot/WiFiP2PHotspotApp.kt index 3e41578..dd03e84 100644 --- a/app/src/main/java/com/example/wifip2photspot/WiFiP2PHotspotApp.kt +++ b/app/src/main/java/com/example/wifip2photspot/WiFiP2PHotspotApp.kt @@ -1,360 +1,50 @@ -// WiFiP2PHotspotApp.kt package com.example.wifip2photspot -import android.Manifest -import android.annotation.SuppressLint -import android.app.Activity + import android.content.Context -import android.content.Intent import android.location.LocationManager -import android.net.Uri -import android.net.VpnService import android.net.wifi.WifiManager -import android.os.Build -import android.provider.Settings -import android.util.Log -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import com.example.wifip2photspot.ui.theme.ThemeToggle - - -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.core.app.ActivityCompat.startActivityForResult -import com.example.wifip2photspot.Proxy.DataUsageSection -import com.example.wifip2photspot.Proxy.ProxyControlSection -import com.example.wifip2photspot.Proxy.ProxyInstructionsSection -import com.example.wifip2photspot.Proxy.ProxyLimitationsSection -import com.example.wifip2photspot.Proxy.ProxyService -import com.example.wifip2photspot.Proxy.ProxyStatusSection - +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.example.wifip2photspot.ui.SettingsScreen +import com.example.wifip2photspot.ui.screens.MainScreen +import com.example.wifip2photspot.viewModel.HotspotViewModel -@SuppressLint("StateFlowValueCalledInComposition") @Composable fun WiFiP2PHotspotApp( - viewModel: HotspotViewModel, activity: Activity, isDarkTheme: Boolean, - onThemeChange: (Boolean) -> Unit + hotspotViewModel: HotspotViewModel, ) { + val navController = rememberNavController() + WiFiP2PHotspotNavHost( + navController = navController, + hotspotViewModel = hotspotViewModel + ) - val context = LocalContext.current - - // Collect state from ViewModel - val ssid by viewModel.ssid.collectAsState() - val password by viewModel.password.collectAsState() - val selectedBand by viewModel.selectedBand.collectAsState() - val isHotspotEnabled by viewModel.isHotspotEnabled.collectAsState() - val isProcessing by viewModel.isProcessing.collectAsState() - val uploadSpeed by viewModel.uploadSpeed.collectAsState() - val downloadSpeed by viewModel.downloadSpeed.collectAsState() - val connectedDevices by viewModel.connectedDevices.collectAsState() - val logEntries by viewModel.logEntries.collectAsState() - var isDarkTheme by rememberSaveable { mutableStateOf(false) } - // Local state for TextFieldValue - var ssidFieldState by rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue(ssid)) - } - var passwordFieldState by rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue(password)) - } - - // Dialog State - var showServiceEnableDialog by remember { mutableStateOf(false) } - var showSettingsDialog by remember { mutableStateOf(false) } - - - val connectedDeviceInfos by viewModel.connectedDeviceInfos.collectAsState() - // Collect the blocked devices from the ViewModel - val blockedDeviceInfos by viewModel.blockedDeviceInfos.collectAsState() - - val (sessionRxBytes, sessionTxBytes) = viewModel.getSessionDataUsage() - val uploadSpeedEntries by viewModel.uploadSpeedEntries.collectAsState() - val downloadSpeedEntries by viewModel.downloadSpeedEntries.collectAsState() - - - - -//// Proxy server state -// val isProxyRunning by viewModel.isProxyRunning.collectAsState() - val proxyPort by viewModel.proxyPort.collectAsState() -// val proxyIP = "192.168.49.1" // Typically the group owner's IP in Wi-Fi Direct -// - - // Update ViewModel when text changes - LaunchedEffect(ssidFieldState.text) { - viewModel.updateSSID(ssidFieldState.text) - } - LaunchedEffect(passwordFieldState.text) { - viewModel.updatePassword(passwordFieldState.text) - } - - LaunchedEffect(connectedDeviceInfos) { - Log.d( - "WiFiP2PHotspotApp", - "ConnectedDeviceInfos updated: ${connectedDeviceInfos.size} devices" - ) - } - // Handle UI Events - LaunchedEffect(key1 = true) { - viewModel.eventFlow.collect { event -> - when (event) { - is HotspotViewModel.UiEvent.ShowToast -> { - Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() - } - - is HotspotViewModel.UiEvent.ShowSnackbar -> { - // Implement Snackbar if needed - } +} - is HotspotViewModel.UiEvent.StartProxyService -> { - val intent = Intent(context, ProxyService::class.java) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(intent) - } else { - context.startService(intent) - } - } +@Composable +fun WiFiP2PHotspotNavHost( + navController: NavHostController, + hotspotViewModel: HotspotViewModel +) { + NavHost(navController = navController, startDestination = "main_screen") { + composable("main_screen") { + MainScreen( + navController = navController, + hotspotViewModel = hotspotViewModel + ) - is HotspotViewModel.UiEvent.StopProxyService -> { - val intent = Intent(context, ProxyService::class.java) - context.stopService(intent) - } - } } - } - - - // Scaffold for overall layout - Scaffold( - topBar = { ImprovedHeader(isHotspotEnabled = isHotspotEnabled) }, - content = { paddingValues -> - LazyColumn( - contentPadding = paddingValues, - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp) - ) { - // Theme Toggle - item { - ThemeToggle( - isDarkTheme = isDarkTheme, - onToggle = onThemeChange - ) - Spacer(modifier = Modifier.height(8.dp)) - } - - // Input Fields and Band Selection - if (connectedDeviceInfos.isEmpty()) { - item { - InputFieldsSection( - ssidInput = ssidFieldState, - onSsidChange = { newValue -> - ssidFieldState = newValue - }, - passwordInput = passwordFieldState, - onPasswordChange = { newValue -> - passwordFieldState = newValue - }, - isHotspotEnabled = isHotspotEnabled, - proxyPort = proxyPort, - onProxyPortChange = { newPort -> - viewModel.updateProxyPort(newPort) - }, - selectedBand = selectedBand, - onBandSelected = { newBand -> - viewModel.updateSelectedBand(newBand) - }, - bands = listOf("Auto", "2.4GHz", "5GHz") - ) - } - - - } else { - item { - ConnectionStatusBar( - uploadSpeed = uploadSpeed, - downloadSpeed = downloadSpeed, - totalDownload = downloadSpeed, // Adjust if you have a separate totalDownload - connectedDevicesCount = connectedDevices.size - ) - } - item { - DataUsageSection( - rxBytes = sessionRxBytes, - txBytes = sessionTxBytes - ) - } -// item { -// HistoricalDataUsageSection(historicalData = viewModel.historicalDataUsage.value) -// } - item { - SpeedGraphSection( - uploadSpeeds = uploadSpeedEntries, - downloadSpeeds = downloadSpeedEntries - ) - } - } - - item { - Spacer(modifier = Modifier.height(16.dp)) - } - - // Hotspot Control Section - item { - if (isWifiEnabled(context) && isLocationEnabled(context)) { - HotspotControlSection( - isHotspotEnabled = isHotspotEnabled, - isProcessing = isProcessing, - ssidInput = ssidFieldState.text, - passwordInput = passwordFieldState.text, - selectedBand = selectedBand, - proxyPort = proxyPort, - onStartTapped = { - viewModel.onButtonStartTapped( - ssidInput = ssidFieldState.text.ifBlank { "TetherGuard" }, - passwordInput = passwordFieldState.text.ifBlank { "00000000" }, - selectedBand = selectedBand, - ) - }, - onStopTapped = { - viewModel.onButtonStopTapped() - } - ) - - - } else { - showServiceEnableDialog = true - } - } - item { - Spacer(modifier = Modifier.height(16.dp)) - } - item { - NotificationSettingsSection( - notificationEnabled = viewModel.notificationEnabled.value, - onNotificationEnabledChange = { viewModel.updateNotificationEnabled(it) }, - soundEnabled = viewModel.notificationSoundEnabled.value, - onSoundEnabledChange = { viewModel.updateNotificationSoundEnabled(it) }, - vibrationEnabled = viewModel.notificationVibrationEnabled.value, - onVibrationEnabledChange = { viewModel.updateNotificationVibrationEnabled(it) } - ) - } - - if (isHotspotEnabled) { - // Connected Devices Section - connectedDevicesSection( - devices = connectedDeviceInfos, - onDeviceAliasChange = { deviceAddress, alias -> - viewModel.updateDeviceAlias(deviceAddress, alias) - }, - onBlockUnblock = { deviceAddress -> - val deviceInfo = - connectedDeviceInfos.find { it.device.deviceAddress == deviceAddress } - if (deviceInfo != null) { - if (deviceInfo.isBlocked) { - viewModel.unblockDevice(deviceAddress) - } else { - viewModel.blockDevice(deviceAddress) - } - } - }, - onDisconnect = { deviceAddress -> - val deviceInfo = - connectedDeviceInfos.find { it.device.deviceAddress == deviceAddress } - if (deviceInfo != null) { - viewModel.disconnectDevice(deviceInfo) - } - } - ) - if(blockedDeviceInfos.isNotEmpty()) { - blockedDevicesSection( - devices = blockedDeviceInfos, - onUnblock = { deviceAddress -> - viewModel.unblockDevice(deviceAddress) - } - ) - } - } -// item { -// HotspotScheduler( -// onScheduleStart = { timeInMillis -> -// viewModel.scheduleHotspotStart(timeInMillis) -// }, -// onScheduleStop = { timeInMillis -> -// viewModel.scheduleHotspotStop(timeInMillis) -// } -// ) -// } - - item { - Spacer(modifier = Modifier.height(16.dp)) - } - - // Log Section - item { - LogSection(logEntries = logEntries) - } - item { - FeedbackForm(onSubmit = { feedback -> - viewModel.submitFeedback(feedback) - }) - } - item { - ContactSupportSection(onContactSupport = { - viewModel.contactSupport() - }) - } - } + composable("settings_screen") { + SettingsScreen( + navController = navController, + hotspotViewModel = hotspotViewModel + ) } - ) -} - - -/** - * Checks if all required permissions are granted. - */ -//fun arePermissionsGranted(context: Context): Boolean { -// val fineLocation = ContextCompat.checkSelfPermission( -// context, Manifest.permission.ACCESS_FINE_LOCATION -// ) == android.content.pm.PackageManager.PERMISSION_GRANTED -// -// val wifiState = ContextCompat.checkSelfPermission( -// context, Manifest.permission.ACCESS_WIFI_STATE -// ) == android.content.pm.PackageManager.PERMISSION_GRANTED -// -// val changeWifiState = ContextCompat.checkSelfPermission( -// context, Manifest.permission.CHANGE_WIFI_STATE -// ) == android.content.pm.PackageManager.PERMISSION_GRANTED -// -// return fineLocation && wifiState && changeWifiState -//} - -/** - * Opens the device's Wi-Fi settings. - */ -fun openDeviceSettings(context: Context) { - val intent = Intent(Settings.ACTION_WIFI_SETTINGS) - context.startActivity(intent) -} - -/** - * Opens the app-specific settings screen. - */ -fun openAppSettings(context: Context) { - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) - intent.data = Uri.fromParts("package", context.packageName, null) - context.startActivity(intent) + } } /** @@ -377,94 +67,3 @@ fun isLocationEnabled(context: Context): Boolean { ) == true } -/** - * Launches settings if services are enabled. - */ -//fun launchSettingsIfServicesEnabled(context: Context) { -// if (arePermissionsGranted(context)) { -// if (isWifiEnabled(context) && isLocationEnabled(context)) { -// openDeviceSettings(context) -// } else { -// // Optionally, prompt to enable services -// Toast.makeText( -// context, "Please enable Wi-Fi and Location services.", Toast.LENGTH_SHORT -// ).show() -// } -// } else { -// // Optionally, prompt for permissions again or inform the user -// Toast.makeText(context, "Permissions are not granted.", Toast.LENGTH_SHORT).show() -// } -//} - - -@Composable -fun StartVpnButton(onStartVpn: () -> Unit) { - Button(onClick = onStartVpn) { - Text("Start VPN") - } -} -//// Launcher for requesting multiple permissions -// val permissionLauncher = rememberLauncherForActivityResult( -// contract = ActivityResultContracts.RequestMultiplePermissions() -// ) { permissions -> -// val allGranted = permissions.all { it.value } -// if (allGranted) { -// Toast.makeText( -// context, context.getString(R.string.permissions_granted), Toast.LENGTH_SHORT -// ).show() -// if (!isWifiEnabled(context) || !isLocationEnabled(context)) { -// showServiceEnableDialog = true -// } -// } else { -// // Use the Activity instance to call shouldShowRequestPermissionRationale -// val permanentlyDenied = -// permissions.any { !it.value && !activity.shouldShowRequestPermissionRationale(it.key) } -// if (permanentlyDenied) { -// showSettingsDialog = true -// } else { -// Toast.makeText( -// context, context.getString(R.string.permissions_denied), Toast.LENGTH_SHORT -// ).show() -// } -// } -// } -// -//// Show Service Enable Dialog -// if (showServiceEnableDialog) { -// AlertDialog(onDismissRequest = { showServiceEnableDialog = false }, -// title = { Text(text = stringResource(id = R.string.enable_services)) }, -// text = { Text(text = stringResource(id = R.string.enable_wifi_location_services)) }, -// confirmButton = { -// TextButton(onClick = { -// showServiceEnableDialog = false -// openDeviceSettings(context) -// }) { -// Text(text = stringResource(id = R.string.go_to_settings)) -// } -// }, -// dismissButton = { -// TextButton(onClick = { showServiceEnableDialog = false }) { -// Text(text = stringResource(id = R.string.cancel)) -// } -// }) -// } -// -//// Show Settings Dialog -// if (showSettingsDialog) { -// AlertDialog(onDismissRequest = { showSettingsDialog = false }, -// title = { Text(text = stringResource(id = R.string.permissions_required)) }, -// text = { Text(text = stringResource(id = R.string.permissions_permanently_denied)) }, -// confirmButton = { -// TextButton(onClick = { -// showSettingsDialog = false -// openAppSettings(context) -// }) { -// Text(text = stringResource(id = R.string.go_to_settings)) -// } -// }, -// dismissButton = { -// TextButton(onClick = { showSettingsDialog = false }) { -// Text(text = stringResource(id = R.string.cancel)) -// } -// }) -// } diff --git a/app/src/main/java/com/example/wifip2photspot/ui/IdleCountdownDisplay.kt b/app/src/main/java/com/example/wifip2photspot/ui/IdleCountdownDisplay.kt new file mode 100644 index 0000000..eef95d0 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ui/IdleCountdownDisplay.kt @@ -0,0 +1,78 @@ +package com.example.wifip2photspot.ui +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.style.TextAlign +import com.example.wifip2photspot.SwitchPreference + +@Composable +fun IdleCountdownDisplay(remainingIdleTime: Long) { + if (remainingIdleTime > 0L) { + val minutes = (remainingIdleTime / 1000) / 60 + val seconds = (remainingIdleTime / 1000) % 60 + val timeString = String.format("%02d:%02d", minutes, seconds) + + Card( + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(16.dp) + ) { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = "Idle Warning", + tint = MaterialTheme.colorScheme.onErrorContainer + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Hotspot will shut down in: $timeString", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onErrorContainer, + textAlign = TextAlign.Center + ) + } + } + } +} + + + +@Composable +fun IdleSettingsSection( + autoShutdownEnabled: Boolean, + onAutoShutdownEnabledChange: (Boolean) -> Unit, + idleTimeoutMinutes: Int, + onIdleTimeoutChange: (Int) -> Unit +) { + Column(modifier = Modifier.padding(8.dp)) { + Text("Idle Settings", style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreference( + label = "Auto Shutdown when Idle", + checked = autoShutdownEnabled, + onCheckedChange = onAutoShutdownEnabledChange + ) + + if (autoShutdownEnabled) { + Spacer(modifier = Modifier.height(8.dp)) + Text("Idle Timeout (minutes): $idleTimeoutMinutes", style = MaterialTheme.typography.bodyLarge) + Slider( + value = idleTimeoutMinutes.toFloat(), + onValueChange = { onIdleTimeoutChange(it.toInt()) }, + valueRange = 1f..60f, + steps = 59 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/wifip2photspot/ui/SettingsContent.kt b/app/src/main/java/com/example/wifip2photspot/ui/SettingsContent.kt new file mode 100644 index 0000000..e39fbdc --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ui/SettingsContent.kt @@ -0,0 +1,100 @@ +package com.example.wifip2photspot.ui + + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.example.wifip2photspot.BandSelection +import com.example.wifip2photspot.ContactSupportSection +import com.example.wifip2photspot.FeedbackForm +import com.example.wifip2photspot.viewModel.HotspotViewModel +import com.example.wifip2photspot.ui.theme.ThemeToggle + +@Composable +fun SettingsContent( + hotspotViewModel: HotspotViewModel, + paddingValues: PaddingValues +) { + // Collect necessary state from ViewModel + val isDarkTheme by hotspotViewModel.isDarkTheme.collectAsState() + val autoShutdownEnabled by hotspotViewModel.autoShutdownEnabled.collectAsState() + val idleTimeoutMinutes by hotspotViewModel.idleTimeoutMinutes.collectAsState() + val wifiLockEnabled by hotspotViewModel.wifiLockEnabled.collectAsState() + val selectedBand by hotspotViewModel.selectedBand.collectAsState() + val isHotspotEnabled by hotspotViewModel.isHotspotEnabled.collectAsState() + + + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(16.dp)) + + // Dark Theme Toggle + ThemeToggle( + isDarkTheme = isDarkTheme, + onToggle = { isDark -> + hotspotViewModel.updateTheme(isDark) + } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Spacer(modifier = Modifier.height(16.dp)) + + // Idle Settings + IdleSettingsSection( + autoShutdownEnabled = autoShutdownEnabled, + onAutoShutdownEnabledChange = { hotspotViewModel.updateAutoShutdownEnabled(it) }, + idleTimeoutMinutes = idleTimeoutMinutes, + onIdleTimeoutChange = { hotspotViewModel.updateIdleTimeoutMinutes(it) } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Wi-Fi Lock Settings + WifiLockSettingsSection( + wifiLockEnabled = wifiLockEnabled, + onWifiLockEnabledChange = { hotspotViewModel.updateWifiLockEnabled(it) } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Band Selection (if applicable) + BandSelection( + selectedBand = selectedBand, + onBandSelected = { hotspotViewModel.updateSelectedBand(it) }, + bands = listOf("Auto", "2.4GHz", "5GHz"), + isHotspotEnabled = isHotspotEnabled + ) + + Spacer(modifier = Modifier.height(16.dp)) + + + Spacer(modifier = Modifier.height(16.dp)) + + + Spacer(modifier = Modifier.height(16.dp)) + + // Feedback Form + FeedbackForm(onSubmit = { feedback -> + hotspotViewModel.submitFeedback(feedback) + }) + + Spacer(modifier = Modifier.height(16.dp)) + + // Contact Support Section + ContactSupportSection(onContactSupport = { + hotspotViewModel.contactSupport() + }) + + Spacer(modifier = Modifier.height(16.dp)) + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/wifip2photspot/ui/SettingsScreen.kt b/app/src/main/java/com/example/wifip2photspot/ui/SettingsScreen.kt new file mode 100644 index 0000000..444e18d --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ui/SettingsScreen.kt @@ -0,0 +1,37 @@ +package com.example.wifip2photspot.ui + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.navigation.NavHostController +import com.example.wifip2photspot.viewModel.HotspotViewModel + +@OptIn(ExperimentalMaterial3Api::class) + +@Composable +fun SettingsScreen( + navController: NavHostController, + hotspotViewModel: HotspotViewModel +){ + Scaffold( + topBar = { + TopAppBar( + title = { Text("Settings") }, + navigationIcon = { + IconButton(onClick = { navController.navigateUp() }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + } + ) + }, + content = { paddingValues -> + SettingsContent( + hotspotViewModel = hotspotViewModel, + paddingValues = paddingValues + ) + } + ) +} + + diff --git a/app/src/main/java/com/example/wifip2photspot/ui/WifiLockSettingsSection.kt b/app/src/main/java/com/example/wifip2photspot/ui/WifiLockSettingsSection.kt new file mode 100644 index 0000000..d1cb630 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ui/WifiLockSettingsSection.kt @@ -0,0 +1,34 @@ +package com.example.wifip2photspot.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.example.wifip2photspot.SwitchPreference + + +@Composable +fun WifiLockSettingsSection( + wifiLockEnabled: Boolean, + onWifiLockEnabledChange: (Boolean) -> Unit +) { + Column(modifier = Modifier.padding(8.dp)) { + Text("Wi-Fi Lock", style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + SwitchPreference( + label = "Keep Wi-Fi Awake", + checked = wifiLockEnabled, + onCheckedChange = onWifiLockEnabledChange + ) + Text( + text = "Enable this to prevent the Wi-Fi from going to sleep while the hotspot is active.", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(top = 4.dp) + ) + } +} diff --git a/app/src/main/java/com/example/wifip2photspot/ui/screens/MainScreen.kt b/app/src/main/java/com/example/wifip2photspot/ui/screens/MainScreen.kt new file mode 100644 index 0000000..f5b466f --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ui/screens/MainScreen.kt @@ -0,0 +1,235 @@ +package com.example.wifip2photspot.ui.screens + + +import android.annotation.SuppressLint +import android.widget.Toast +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.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import com.example.wifip2photspot.BatteryStatusSection +import com.example.wifip2photspot.ConnectionStatusBar +import com.example.wifip2photspot.HotspotControlSection +import com.example.wifip2photspot.ImprovedHeader +import com.example.wifip2photspot.InputFieldsSection +import com.example.wifip2photspot.LogSection +import com.example.wifip2photspot.blockedDevicesSection +import com.example.wifip2photspot.connectedDevicesSection +import com.example.wifip2photspot.isLocationEnabled +import com.example.wifip2photspot.isWifiEnabled +import com.example.wifip2photspot.ui.IdleCountdownDisplay +import com.example.wifip2photspot.viewModel.HotspotViewModel +import timber.log.Timber + +@SuppressLint("StateFlowValueCalledInComposition", "UnrememberedMutableState", "TimberArgCount") +@Composable +fun MainScreen( + navController: NavHostController, hotspotViewModel: HotspotViewModel +) { + val context = LocalContext.current + + val ssid by hotspotViewModel.ssid.collectAsState() + val password by hotspotViewModel.password.collectAsState() + val selectedBand by hotspotViewModel.selectedBand.collectAsState() + // Collect state from ViewModels + val isHotspotEnabled by hotspotViewModel.isHotspotEnabled.collectAsState() + val isProcessing by hotspotViewModel.isProcessing.collectAsState() + val connectedDevices by hotspotViewModel.connectedDevices.collectAsState() + val logEntries by hotspotViewModel.logEntries.collectAsState() + val remainingIdleTime by hotspotViewModel.remainingIdleTime.collectAsState() + + // Local state for TextFieldValue + var ssidFieldState by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue(ssid)) + } + var passwordFieldState by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue(password)) + } + // Dialog State + var showServiceEnableDialog by remember { mutableStateOf(false) } + val connectedDeviceInfos by hotspotViewModel.connectedDeviceInfos.collectAsState() + // Collect the blocked devices from the hotspotViewModel + val blockedDeviceInfos by hotspotViewModel.blockedDeviceInfos.collectAsState() + val batteryLevel by hotspotViewModel.batteryLevel.collectAsState() + val proxyStatus by hotspotViewModel.proxyStatus.collectAsState() + + + // Update hotspotViewModel when text changes + LaunchedEffect(ssidFieldState.text) { + hotspotViewModel.updateSSID(ssidFieldState.text) + } + LaunchedEffect(passwordFieldState.text) { + hotspotViewModel.updatePassword(passwordFieldState.text) + } + LaunchedEffect(connectedDeviceInfos) { + Timber.tag("WiFiP2PHotspotApp") + .d("%s devices", "ConnectedDeviceInfos updated: %s", connectedDeviceInfos.size) + } + // Start idle monitoring when the hotspot is enabled + LaunchedEffect(isHotspotEnabled) { + if (isHotspotEnabled) { + hotspotViewModel.startIdleMonitoring() + } + } + // Scaffold for overall layout + Scaffold(topBar = { + ImprovedHeader( + onSettingsClick = { navController.navigate("settings_screen") }, + ) + }, content = { paddingValues -> + LazyColumn( + contentPadding = paddingValues, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + item { + Spacer(modifier = Modifier.height(16.dp)) + } + + // Display Idle Countdown if applicable + item { + IdleCountdownDisplay(remainingIdleTime = remainingIdleTime) + + } + item { + Spacer(modifier = Modifier.height(16.dp)) + } + // Proxy Status Display + if (isHotspotEnabled) { + item { + Text( + text = "Proxy Status: $proxyStatus", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) + } + } + // Input Fields and Band Selection + if (!isHotspotEnabled) { + item { + InputFieldsSection(ssidInput = TextFieldValue(ssid), + onSsidChange = { newValue -> + hotspotViewModel.updateSSID(newValue.text) + }, + passwordInput = TextFieldValue(password), + onPasswordChange = { newValue -> + hotspotViewModel.updatePassword(newValue.text) + }, + isHotspotEnabled = isHotspotEnabled + ) + } + } else { + item { + Text( + text = "Hotspot is enabled with SSID: $ssid", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) + } + } +// +// item { +// ConnectionStatusBar( +// uploadSpeed = uploadSpeed, +// downloadSpeed = downloadSpeed, +// totalDownload = downloadSpeed, // Adjust if you have a separate totalDownload +// connectedDevicesCount = connectedDevices.size +// ) +// } +// item { +// BatteryStatusSection(batteryLevel = batteryLevel) +// } +// item { +// Spacer(modifier = Modifier.height(16.dp)) +// } + // Hotspot Control Section + item { + if (isWifiEnabled(context) && isLocationEnabled(context)) { + HotspotControlSection(isHotspotEnabled = isHotspotEnabled, + isProcessing = isProcessing, + onStartTapped = { + hotspotViewModel.onButtonStartTapped( + ssidInput = ssidFieldState.text.ifBlank { "TetherGuard" }, + passwordInput = passwordFieldState.text.ifBlank { "00000000" }, + selectedBand = selectedBand, + ) + }, + onStopTapped = { + hotspotViewModel.onButtonStopTapped() + }) + + } else { + showServiceEnableDialog = true + } + } + item { + Spacer(modifier = Modifier.height(16.dp)) + } + if (isHotspotEnabled) { + // Connected Devices Section + connectedDevicesSection(devices = connectedDeviceInfos, + onDeviceAliasChange = { deviceAddress, alias -> + hotspotViewModel.updateDeviceAlias(deviceAddress, alias) + }, + onBlockUnblock = { deviceAddress -> + val deviceInfo = + connectedDeviceInfos.find { it.device.deviceAddress == deviceAddress } + if (deviceInfo != null) { + if (deviceInfo.isBlocked) { + hotspotViewModel.unblockDevice(deviceAddress) + } else { + hotspotViewModel.blockDevice(deviceAddress) + } + } + }, + onDisconnect = { deviceAddress -> + val deviceInfo = + connectedDeviceInfos.find { it.device.deviceAddress == deviceAddress } + if (deviceInfo != null) { + hotspotViewModel.disconnectDevice(deviceInfo) + } + }) + if (blockedDeviceInfos.isNotEmpty()) { + blockedDevicesSection(devices = blockedDeviceInfos, + onUnblock = { deviceAddress -> + hotspotViewModel.unblockDevice(deviceAddress) + }) + } + } + // Display Connected Devices + item { + Spacer(modifier = Modifier.height(16.dp)) + } + // Log Section + item { + LogSection(logEntries = logEntries) + } + + } + + }) +} + diff --git a/app/src/main/java/com/example/wifip2photspot/ui/screens/PermissionDialogs.kt b/app/src/main/java/com/example/wifip2photspot/ui/screens/PermissionDialogs.kt new file mode 100644 index 0000000..bc9bf26 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ui/screens/PermissionDialogs.kt @@ -0,0 +1,43 @@ +// File: PermissionDialogs.kt +package com.example.wifip2photspot.ui.screens + +import android.Manifest +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable + +@Composable +fun PermissionDeniedDialog( + deniedPermissions: List, + onDismiss: () -> Unit, + onConfirm: () -> Unit, + onOpenSettings: () -> Unit +) { + val message = buildString { + append("The app requires the following permissions for proper functionality:\n\n") + deniedPermissions.forEach { permission -> + when (permission) { + Manifest.permission.ACCESS_FINE_LOCATION -> append("- Access Fine Location\n") + Manifest.permission.NEARBY_WIFI_DEVICES -> append("- Nearby Wi-Fi Devices\n") + else -> append("- $permission\n") + } + } + append("\nPlease grant these permissions.") + } + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Permissions Required") }, + text = { Text(message) }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text("Grant Permissions") + } + }, + dismissButton = { + TextButton(onClick = onOpenSettings) { + Text("Open Settings") + } + } + ) +} diff --git a/app/src/main/java/com/example/wifip2photspot/ui/screens/ServiceDisabledDialog.kt b/app/src/main/java/com/example/wifip2photspot/ui/screens/ServiceDisabledDialog.kt new file mode 100644 index 0000000..237e49a --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ui/screens/ServiceDisabledDialog.kt @@ -0,0 +1,30 @@ +// File: ServiceDisabledDialog.kt +package com.example.wifip2photspot.ui.screens + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable + +@Composable +fun ServiceDisabledDialog( + serviceName: String, + onDismiss: () -> Unit, + onConfirm: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("$serviceName Disabled") }, + text = { Text("The app requires $serviceName to be enabled. Please enable $serviceName in settings.") }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text("Open Settings") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) +} diff --git a/app/src/main/java/com/example/wifip2photspot/ui/theme/ThemeToggle.kt b/app/src/main/java/com/example/wifip2photspot/ui/theme/ThemeToggle.kt index 6ba47a0..1808733 100644 --- a/app/src/main/java/com/example/wifip2photspot/ui/theme/ThemeToggle.kt +++ b/app/src/main/java/com/example/wifip2photspot/ui/theme/ThemeToggle.kt @@ -5,8 +5,10 @@ package com.example.wifip2photspot.ui.theme import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.example.wifip2photspot.SwitchPreference @Composable fun ThemeToggle( @@ -16,18 +18,18 @@ fun ThemeToggle( Row( modifier = Modifier .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.End + .padding(vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Text(text = "Dark Theme") - Spacer(modifier = Modifier.width(8.dp)) - Switch( +// Text("Dark Theme", style = MaterialTheme.typography.bodyLarge) + SwitchPreference( + label = "Dark Theme", checked = isDarkTheme, - onCheckedChange = onToggle, - colors = SwitchDefaults.colors( - checkedThumbColor = MaterialTheme.colorScheme.primary, - uncheckedThumbColor = MaterialTheme.colorScheme.onSurface - ) + onCheckedChange = onToggle + ) } } + + diff --git a/app/src/main/java/com/example/wifip2photspot/viewModel/HotspotViewModel.kt b/app/src/main/java/com/example/wifip2photspot/viewModel/HotspotViewModel.kt new file mode 100644 index 0000000..ff2676e --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/viewModel/HotspotViewModel.kt @@ -0,0 +1,719 @@ +// HotspotViewModel.kt +package com.example.wifip2photspot.viewModel + +import android.annotation.SuppressLint +import android.app.Application +import android.content.Context +import android.content.Intent +import android.net.MacAddress +import android.net.Uri +import android.net.wifi.WifiManager +import android.net.wifi.WpsInfo +import android.net.wifi.p2p.WifiP2pConfig +import android.net.wifi.p2p.WifiP2pDevice +import android.net.wifi.p2p.WifiP2pManager +import android.os.Build +import android.os.Looper +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.contentcapture.ContentCaptureManager.Companion.isEnabled +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.* +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.example.wifip2photspot.DeviceInfo +import com.example.wifip2photspot.HttpProxyServer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import timber.log.Timber +import java.io.IOException +import java.util.concurrent.atomic.AtomicBoolean + +class HotspotViewModel( + application: Context, + private val dataStore: DataStore, +) : AndroidViewModel(application as Application) { + // ----- DataStore Keys ----- + companion object { + val SSID_KEY = stringPreferencesKey("ssid") + val PASSWORD_KEY = stringPreferencesKey("password") + private val SELECTED_BAND_KEY = stringPreferencesKey("selected_band") + val AUTO_SHUTDOWN_ENABLED_KEY = booleanPreferencesKey("auto_shutdown_enabled") + val IDLE_TIMEOUT_MINUTES_KEY = intPreferencesKey("idle_timeout_minutes") + private val BLOCKED_MAC_ADDRESSES_KEY = stringSetPreferencesKey("blocked_mac_addresses") + private val DEVICE_ALIAS_KEY = stringPreferencesKey("device_aliases") + private val WIFI_LOCK_ENABLED_KEY = booleanPreferencesKey("wifi_lock_enabled") + private val _deviceAliases = MutableStateFlow>(emptyMap()) + private const val DEFAULT_PROXY_PORT = 8080 + private const val DEFAULT_PROXY_IP = "192.168.49.1" + } + + // ----- Wi-Fi P2P Manager and Channel ----- + private val wifiP2pManager: WifiP2pManager = + application.getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager + private val channel: WifiP2pManager.Channel = + wifiP2pManager.initialize(application, Looper.getMainLooper(), null) + + // ----- StateFlows for UI State ----- + private val _ssid = MutableStateFlow("TetherGuard") + val ssid: StateFlow = _ssid.asStateFlow() + + private val _password = MutableStateFlow("00000000") + val password: StateFlow = _password.asStateFlow() + + private val _selectedBand = MutableStateFlow("Auto") + val selectedBand: StateFlow = _selectedBand.asStateFlow() + + + private val _isWifiP2pEnabled = MutableStateFlow(false) + + private val _isHotspotEnabled = MutableStateFlow(false) + val isHotspotEnabled: StateFlow = _isHotspotEnabled.asStateFlow() + + private val _isProcessing = MutableStateFlow(false) + val isProcessing: StateFlow = _isProcessing.asStateFlow() + + + // Dark Theme State + private val _isDarkTheme = MutableStateFlow(false) + val isDarkTheme: StateFlow = _isDarkTheme.asStateFlow() + fun updateTheme(isDark: Boolean) { + _isDarkTheme.value = isDark + // Save to DataStore if needed + } + // ----- Log Entries ----- + private val _logEntries = MutableStateFlow>(emptyList()) + val logEntries: StateFlow> = _logEntries.asStateFlow() + // ----- UI Events ----- + private val _eventFlow = MutableSharedFlow() + // ----- Connected Devices ----- + private val _connectedDevices = MutableStateFlow>(emptyList()) + val connectedDevices: StateFlow> = _connectedDevices.asStateFlow() + private val _connectedDeviceInfos = MutableStateFlow>(emptyList()) + val connectedDeviceInfos: StateFlow> = _connectedDeviceInfos.asStateFlow() + + // ----- Sealed Class for UI Events ----- + sealed class UiEvent { + data class ShowToast(val message: String) : UiEvent() + } + // Add a new property for blocked devices + private val _blockedMacAddresses = MutableStateFlow>(emptySet()) + private val _blockedDeviceInfos = MutableStateFlow>(emptyList()) + val blockedDeviceInfos: StateFlow> = _blockedDeviceInfos.asStateFlow() + //battery level + private val _batteryLevel = MutableStateFlow(100) + val batteryLevel: StateFlow = _batteryLevel.asStateFlow() + private val _isIdle = MutableStateFlow(false) + + // Idle Settings State + private val _autoShutdownEnabled = MutableStateFlow(false) + val autoShutdownEnabled: StateFlow = _autoShutdownEnabled.asStateFlow() + + private val _idleTimeoutMinutes = MutableStateFlow(10) + val idleTimeoutMinutes: StateFlow = _idleTimeoutMinutes.asStateFlow() + + // Remaining Idle Time State + private val _remainingIdleTime = MutableStateFlow(0L) + val remainingIdleTime: StateFlow = _remainingIdleTime.asStateFlow() + + // 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 _groupInfo = MutableStateFlow?>(null) + val groupInfo: StateFlow?> = _groupInfo + + private val _isGroupOwner = MutableStateFlow(false) + val isGroupOwner: StateFlow = _isGroupOwner.asStateFlow() + + private val _connectionStatus = MutableStateFlow("Disconnected") + val connectionStatus: StateFlow = _connectionStatus.asStateFlow() + + // ----- HTTP Proxy Server ----- + private var proxyServer: HttpProxyServer? = null + + private val _proxyStatus = MutableStateFlow("Stopped") + val proxyStatus: StateFlow = _proxyStatus.asStateFlow() + + private var proxyPort = DEFAULT_PROXY_PORT + private var proxyIp = DEFAULT_PROXY_IP + + // Flag to prevent multiple starts/stops + private val isProxyRunning = AtomicBoolean(false) + + + init { + viewModelScope.launch { + dataStore.data.catch { exception -> + if (exception is IOException) emit(emptyPreferences()) + else throw exception + }.collect { preferences -> + _ssid.value = preferences[SSID_KEY] ?: "TetherGuard" + _password.value = preferences[PASSWORD_KEY] ?: "00000000" + _selectedBand.value = preferences[SELECTED_BAND_KEY] ?: "Auto" + } + } + + // Load other preferences + viewModelScope.launch { + dataStore.data.collect { preferences -> + _autoShutdownEnabled.value = preferences[AUTO_SHUTDOWN_ENABLED_KEY] ?: false + _idleTimeoutMinutes.value = preferences[IDLE_TIMEOUT_MINUTES_KEY] ?: 10 + _wifiLockEnabled.value = preferences[WIFI_LOCK_ENABLED_KEY] ?: false + } + } + // Load blocked devices + viewModelScope.launch { + dataStore.data.collect { preferences -> + val blockedAddresses = preferences[BLOCKED_MAC_ADDRESSES_KEY] ?: emptySet() + _blockedMacAddresses.value = blockedAddresses + updateBlockedDevices() + } + } + // Load device aliases from DataStore + viewModelScope.launch { + dataStore.data.collect { preferences -> + val aliasesJson = preferences[DEVICE_ALIAS_KEY] ?: "{}" + _deviceAliases.value = Json.decodeFromString(aliasesJson) + } + } + } + // ----- Function to Handle BroadcastReceiver Calls ----- + fun onGroupOwnerChanged(isGroupOwner: Boolean) { + viewModelScope.launch { + if (isGroupOwner) { + _isHotspotEnabled.value = true + updateLog("Device is Group Owner. Starting SSH Server and VPN.") + + } else { + _isHotspotEnabled.value = false + updateLog("Device is Client. SSH Server and VPN are not required.") + } + } + } + // ----- Function to Handle Device List Changes ----- + @SuppressLint("TimberArgCount") + fun onDevicesChanged(deviceList: Collection) { + _connectedDevices.value + updateLog("Connected Devices: ${deviceList.size}") + _connectedDevices.value = deviceList.toList() + enforceAccessControl() + // Check for new connections + Timber.tag("HotspotViewModel").d("%s devices", "Devices changed: %s", deviceList.size) + val blockedAddresses = _blockedMacAddresses.value + _connectedDeviceInfos.value = deviceList.map { device -> + val isBlocked = blockedAddresses.contains(device.deviceAddress) + val existingInfo = + _connectedDeviceInfos.value.find { it.device.deviceAddress == device.deviceAddress } + existingInfo?.copy(isBlocked = isBlocked) ?: DeviceInfo( + device = device, + isBlocked = isBlocked + ) + } + } + + // ----- Function to Set Wi-Fi P2P Enabled State ----- + @OptIn(ExperimentalComposeUiApi::class) + fun setWifiP2pEnabled(enabled: Boolean) { + _isWifiP2pEnabled.value = enabled + updateLog("Wi-Fi P2P Enabled: $isEnabled") + } + + // ----- Function to Update Selected Band ----- +// ----- Update Selected Band ----- + fun updateSelectedBand(newBand: String) { + viewModelScope.launch { + dataStore.edit { preferences -> + preferences[SELECTED_BAND_KEY] = newBand + } + _selectedBand.value = newBand + updateLog("Selected band updated to: $newBand") + } + } + + // ----- Function to Update SSID ----- + fun updateSSID(newSSID: String) { + viewModelScope.launch { + dataStore.edit { preferences -> + preferences[SSID_KEY] = newSSID + } + _ssid.value = newSSID + updateLog("SSID updated to: $newSSID") + } + } + + // ----- Function to Update Password ----- + fun updatePassword(newPassword: String) { + viewModelScope.launch { + dataStore.edit { preferences -> + preferences[PASSWORD_KEY] = newPassword + } + _password.value = newPassword + updateLog("Password updated.") + } + } + + // ----- Start Hotspot ----- + fun onButtonStartTapped(ssidInput: String, passwordInput: String, selectedBand: String) { + onButtonStopTapped() // Stop existing hotspot before creating a new one + + viewModelScope.launch { + _isProcessing.value = true + + val ssid = "DIRECT-TG-${ssidInput.trim()}" + val password = passwordInput.trim() + + if (ssid.isEmpty() || password.length !in 8..63) { + _isProcessing.value = false + updateLog("Invalid SSID or password.") + return@launch + } + val band = when (selectedBand) { + "2.4GHz" -> WifiP2pConfig.GROUP_OWNER_BAND_2GHZ + "5GHz" -> WifiP2pConfig.GROUP_OWNER_BAND_5GHZ + else -> WifiP2pConfig.GROUP_OWNER_BAND_AUTO + } + + val config = WifiP2pConfig.Builder() + .setNetworkName(ssid) + .setPassphrase(password) + .enablePersistentMode(false) + .setGroupOperatingBand(WifiP2pConfig.GROUP_OWNER_BAND_AUTO) + .setGroupOperatingBand(band) + .build() + + wifiP2pManager.requestGroupInfo(channel) { group -> + if (group != null) { + wifiP2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener { + override fun onSuccess() { + updateLog("Existing group removed. Creating new group...") + createNewGroup(config) + } + + override fun onFailure(reason: Int) { + handleGroupFailure(reason, "remove") + updateLog("Failed to remove existing group. Creating new group...") + retryGroupRemoval(config) + } + }) + } else { + createNewGroup(config) + } + } + } + } + private fun createNewGroup(config: WifiP2pConfig) { + wifiP2pManager.createGroup(channel, config, object : WifiP2pManager.ActionListener { + override fun onSuccess() { + viewModelScope.launch { + _isHotspotEnabled.value = true + _isProcessing.value = false + updateLog("Hotspot started successfully: SSID=${config.networkName}") + requestGroupInfo() + + // Acquire Wi-Fi Lock after hotspot starts successfully + acquireWifiLock() + + // Start idle monitoring after hotspot is active + startIdleMonitoring() + updateLog("Idle monitoring started.") + + // Start HTTP Proxy Server + startHttpProxy() + } + } + + override fun onFailure(reason: Int) { + handleGroupFailure(reason, "create") + retryGroupCreation(config) + } + }) + } + + private fun handleGroupFailure(reason: Int, action: String) { + val reasonMessage = when (reason) { + WifiP2pManager.BUSY -> "Framework is busy" + WifiP2pManager.ERROR -> "Internal error" + WifiP2pManager.P2P_UNSUPPORTED -> "P2P unsupported" + else -> "Unknown error" + } + updateLog("Failed to $action group: $reasonMessage") + _isProcessing.value = false + } + + private fun requestGroupInfo() { + wifiP2pManager.requestGroupInfo(channel) { group -> + if (group != null) { + _groupInfo.value = Pair(group.networkName, group.passphrase) + _isGroupOwner.value = group.isGroupOwner + updateLog("Group Info: SSID=${group.networkName}, Passphrase=${group.passphrase}") + } else { + _groupInfo.value = null + updateLog("No group info available.") + } + } + } + + private fun retryGroupCreation(config: WifiP2pConfig) { + viewModelScope.launch { + delay(2000L) // Wait 2 seconds before retrying + createNewGroup(config) + } + } + + private fun retryGroupRemoval(config: WifiP2pConfig?) { + viewModelScope.launch { + delay(2000L) // Wait 2 seconds before retrying + wifiP2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener { + override fun onSuccess() { + updateLog("Group removed after retry. Creating new group...") + config?.let { createNewGroup(it) } + } + + override fun onFailure(reason: Int) { + updateLog("Failed to remove group after retry: ${getErrorReason(reason)}") + _isProcessing.value = false + } + }) + } + } + + // ----- Stop Hotspot ----- + fun onButtonStopTapped() { + if (!_isHotspotEnabled.value) return + + viewModelScope.launch { + _isProcessing.value = true + wifiP2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener { + override fun onSuccess() { + viewModelScope.launch { + clearHotspotState() + updateLog("Hotspot stopped successfully.") + + // Release Wi-Fi Lock after stopping the hotspot + releaseWifiLock() + // Stop HTTP Proxy Server + stopHttpProxy() + } + } + override fun onFailure(reason: Int) { + handleGroupFailure(reason, "stop") + retryGroupRemoval(null) + } + }) + } + } + + private fun startHttpProxy() { + try { + proxyServer = HttpProxyServer(proxyPort, proxyIp) + proxyServer?.start() + _proxyStatus.value = "Running on $proxyIp:$proxyPort" + updateLog("HTTP Proxy started on $proxyIp:$proxyPort") + } catch (e: Exception) { + _proxyStatus.value = "Failed to start" + updateLog("Failed to start HTTP Proxy: ${e.message}") + } + } + + private fun stopHttpProxy() { + try { + proxyServer?.stop() + proxyServer = null + _proxyStatus.value = "Stopped" + updateLog("HTTP Proxy stopped.") + } catch (e: Exception) { + updateLog("Error stopping HTTP Proxy: ${e.message}") + } + } + + + private fun getErrorReason(reason: Int): String { + return when (reason) { + WifiP2pManager.BUSY -> "Framework is busy" + WifiP2pManager.ERROR -> "Internal error" + WifiP2pManager.P2P_UNSUPPORTED -> "P2P unsupported" + WifiP2pManager.NO_SERVICE_REQUESTS -> "No service requests" + else -> "Unknown error code: $reason" + } + } + + private fun clearHotspotState() { + _isHotspotEnabled.value = false + _isGroupOwner.value = false + _groupInfo.value = null + _connectedDevices.value = emptyList() + _isProcessing.value = false + } + // ----- Handle Disconnection ----- + fun onDisconnected() { + clearHotspotState() + updateLog("Group disbanded. All devices disconnected.") + } + // ----- Lifecycle Management ----- + override fun onCleared() { + super.onCleared() + releaseWifiLock() + onButtonStopTapped() + updateLog("ViewModel cleared. Resources released.") + } + + // ----- Logging Utility ----- + fun updateLog(message: String) { + viewModelScope.launch { + _logEntries.value += message + } + } + + // Function to update auto shutdown enabled state + fun updateAutoShutdownEnabled(enabled: Boolean) { + _autoShutdownEnabled.value = enabled + viewModelScope.launch { + dataStore.edit { preferences -> + preferences[AUTO_SHUTDOWN_ENABLED_KEY] = enabled + } + } + } + + // Function to update idle timeout minutes + fun updateIdleTimeoutMinutes(minutes: Int) { + _idleTimeoutMinutes.value = minutes + viewModelScope.launch { + dataStore.edit { preferences -> + preferences[IDLE_TIMEOUT_MINUTES_KEY] = minutes + } + } + } + + // WifiLock Management Functions + fun updateWifiLockEnabled(enabled: Boolean) { + _wifiLockEnabled.value = enabled + viewModelScope.launch { + dataStore.edit { preferences -> + preferences[WIFI_LOCK_ENABLED_KEY] = enabled + } + } + } + + // ----- Wi-Fi Lock ----- + fun acquireWifiLock() { + if (wifiLock == null) { + val wifiManager = getApplication().getSystemService(Context.WIFI_SERVICE) as WifiManager + wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "WiFiP2PHotspotLock") + } + wifiLock?.takeIf { !it.isHeld }?.acquire() + } + + fun releaseWifiLock() { + wifiLock?.takeIf { it.isHeld }?.release() + wifiLock = null + } + + fun startIdleMonitoring() { + viewModelScope.launch { + var idleStartTime = System.currentTimeMillis() // Track when the hotspot becomes idle + while (isHotspotEnabled.value && autoShutdownEnabled.value) { + delay(1000L) // Check every second + val connectedDevices = _connectedDevices.value // Get the current list of connected devices + + // Determine if the hotspot is idle (no connected devices) + _isIdle.value = connectedDevices.isEmpty() + + if (_isIdle.value && _autoShutdownEnabled.value) { + val elapsedIdleTime = System.currentTimeMillis() - idleStartTime + val totalIdleTime = _idleTimeoutMinutes.value * 60 * 1000L + + // Update the remaining idle time + _remainingIdleTime.value = totalIdleTime - elapsedIdleTime + + // If idle time exceeds the allowed timeout, stop the hotspot + if (_remainingIdleTime.value <= 0L) { + withContext(Dispatchers.Main) { + onButtonStopTapped() + _eventFlow.emit(UiEvent.ShowToast("Hotspot turned off due to inactivity")) + } + break + } + } else { + // Reset idle start time if activity is detected + idleStartTime = System.currentTimeMillis() + _remainingIdleTime.value = _idleTimeoutMinutes.value * 60 * 1000L + } + } + // Reset remaining idle time when monitoring stops + _remainingIdleTime.value = 0L + } + } + + + // Function to block a device + fun blockDevice(deviceAddress: String) { + // Add to blocked addresses + val updatedSet = _blockedMacAddresses.value + deviceAddress + _blockedMacAddresses.value = updatedSet + + // Save to DataStore + viewModelScope.launch { + dataStore.edit { preferences -> + preferences[BLOCKED_MAC_ADDRESSES_KEY] = updatedSet + } + } + + // Update blocked devices list + updateBlockedDevices() + + // Enforce access control + enforceAccessControl() + } + + // Function to unblock a device + fun unblockDevice(deviceAddress: String) { + // Remove from blocked addresses + val updatedSet = _blockedMacAddresses.value - deviceAddress + _blockedMacAddresses.value = updatedSet + + // Save to DataStore + viewModelScope.launch { + dataStore.edit { preferences -> + preferences[BLOCKED_MAC_ADDRESSES_KEY] = updatedSet + } + } + + // Update blocked devices list + updateBlockedDevices() + } + + // Function to load blocked devices + private fun updateBlockedDevices() { + _blockedDeviceInfos.value = _blockedMacAddresses.value.map { macAddress -> + DeviceInfo( + device = WifiP2pDevice().apply { deviceAddress = macAddress }, + isBlocked = true + ) + } + } + + // Enforce access control based on blocked devices + private fun enforceAccessControl() { + val devicesToDisconnect = _connectedDeviceInfos.value.filter { deviceInfo -> + deviceInfo.isBlocked + } + + devicesToDisconnect.forEach { deviceInfo -> + disconnectDevice(deviceInfo) + } + } + + fun disconnectDevice(deviceInfo: DeviceInfo) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + try { + // Convert the string MAC address to a MacAddress object for API 33+ + val macAddress = MacAddress.fromString(deviceInfo.device.deviceAddress) + + // Create the Wi-Fi P2P configuration for the specific device + val config = WifiP2pConfig.Builder() + .setDeviceAddress(macAddress) + .build() + + cancelConnection(config, deviceInfo) + } catch (e: IllegalArgumentException) { + updateLog("Invalid MAC address format for device: ${deviceInfo.device.deviceAddress}") + } + } else { + // For API levels below 33, use the WifiP2pConfig constructor + val config = WifiP2pConfig().apply { + deviceAddress = deviceInfo.device.deviceAddress + wps.setup = WpsInfo.PBC // Set WPS setup method + } + + cancelConnection(config, deviceInfo) + } + } + private fun cancelConnection(config: WifiP2pConfig, deviceInfo: DeviceInfo) { + wifiP2pManager.cancelConnect(channel, object : WifiP2pManager.ActionListener { + override fun onSuccess() { + updateLog("Successfully disconnected device: ${deviceInfo.device.deviceName}") + + // Update connected devices list by removing the disconnected device + _connectedDeviceInfos.value = _connectedDeviceInfos.value.filterNot { + it.device.deviceAddress == deviceInfo.device.deviceAddress + } + } + override fun onFailure(reason: Int) { + val reasonStr = when (reason) { + WifiP2pManager.BUSY -> "Framework is busy" + WifiP2pManager.ERROR -> "Internal error" + WifiP2pManager.P2P_UNSUPPORTED -> "P2P unsupported" + else -> "Unknown error" + } + updateLog("Failed to disconnect device: ${deviceInfo.device.deviceName}. Reason: $reasonStr") + } + }) + } + + + // // Update alias function + fun updateDeviceAlias(deviceAddress: String, alias: String) { + // Update aliases map + val updatedAliases = _deviceAliases.value.toMutableMap() + updatedAliases[deviceAddress] = alias + _deviceAliases.value = updatedAliases + + // Save to DataStore + val aliasesJson = Json.encodeToString(updatedAliases) + viewModelScope.launch { + dataStore.edit { preferences -> + preferences[DEVICE_ALIAS_KEY] = aliasesJson + } + } + // Update connected devices + _connectedDeviceInfos.value = _connectedDeviceInfos.value.map { deviceInfo -> + if (deviceInfo.device.deviceAddress == deviceAddress) { + deviceInfo.copy(alias = alias) + } else { + deviceInfo + } + } + } + + + fun contactSupport() { + val intent = Intent(Intent.ACTION_SENDTO).apply { + data = Uri.parse("mailto:") + putExtra(Intent.EXTRA_EMAIL, arrayOf("2024@example.com")) + putExtra(Intent.EXTRA_SUBJECT, "Support Request") + } + intent.resolveActivity(getApplication().packageManager)?.let { + getApplication().startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + } ?: run { + viewModelScope.launch { + _eventFlow.emit(UiEvent.ShowToast("No email client available")) + } + } + } + + fun submitFeedback(feedback: String) { + // For example, open an email intent + val intent = Intent(Intent.ACTION_SENDTO).apply { + data = Uri.parse("mailto:") + putExtra(Intent.EXTRA_EMAIL, arrayOf("support@example.com")) + putExtra(Intent.EXTRA_SUBJECT, "App Feedback") + putExtra(Intent.EXTRA_TEXT, feedback) + } + intent.resolveActivity(getApplication().packageManager)?.let { + getApplication().startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + } ?: run { + // Handle the case where no email client is available + viewModelScope.launch { + _eventFlow.emit(UiEvent.ShowToast("No email client available")) + } + } + } +} diff --git a/app/src/main/java/com/example/wifip2photspot/HotspotViewModelFactory.kt b/app/src/main/java/com/example/wifip2photspot/viewModel/HotspotViewModelFactory.kt similarity index 94% rename from app/src/main/java/com/example/wifip2photspot/HotspotViewModelFactory.kt rename to app/src/main/java/com/example/wifip2photspot/viewModel/HotspotViewModelFactory.kt index a236b84..ccbce07 100644 --- a/app/src/main/java/com/example/wifip2photspot/HotspotViewModelFactory.kt +++ b/app/src/main/java/com/example/wifip2photspot/viewModel/HotspotViewModelFactory.kt @@ -1,5 +1,5 @@ // HotspotViewModelFactory.kt -package com.example.wifip2photspot +package com.example.wifip2photspot.viewModel import android.app.Application import androidx.datastore.core.DataStore @@ -19,4 +19,3 @@ class HotspotViewModelFactory( throw IllegalArgumentException("Unknown ViewModel class") } } - diff --git a/app/src/main/java/com/example/wifip2photspot/viewModel/WiFiDirectBroadcastReceiver.kt b/app/src/main/java/com/example/wifip2photspot/viewModel/WiFiDirectBroadcastReceiver.kt new file mode 100644 index 0000000..c81ff74 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/viewModel/WiFiDirectBroadcastReceiver.kt @@ -0,0 +1,87 @@ +// WifiDirectBroadcastReceiver.kt +package com.example.wifip2photspot.viewModel + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.net.NetworkInfo +import android.net.wifi.p2p.WifiP2pManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import timber.log.Timber + + +class WifiDirectBroadcastReceiver( + private val manager: WifiP2pManager, + private val channel: WifiP2pManager.Channel, + private val viewModel: HotspotViewModel +) : BroadcastReceiver() { + + private val TAG = "WifiDirectBroadcastReceiver" + + override fun onReceive(context: Context?, intent: Intent?) { + val action = intent?.action + + when (action) { + WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> { + // Check if Wi-Fi P2P is enabled + val state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1) + val isEnabled = state == WifiP2pManager.WIFI_P2P_STATE_ENABLED + viewModel.setWifiP2pEnabled(isEnabled) + viewModel.updateLog("Wi-Fi P2P Enabled: $isEnabled") + } + + WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> { + // Respond to new connection or disconnections + Timber.tag(TAG).d("WIFI_P2P_CONNECTION_CHANGED_ACTION received") + val networkInfo = intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO) + if (networkInfo != null && networkInfo.isConnected) { + // Connected to a P2P network + manager.requestConnectionInfo(channel) { info -> + if (info.groupFormed && info.isGroupOwner) { + // Inform ViewModel that the device is now the Group Owner + Timber.tag(TAG).d("Group Owner: %s", info.groupOwnerAddress.hostAddress) + CoroutineScope(Dispatchers.Main).launch { + viewModel.onGroupOwnerChanged(true) + } + + // Request group info and update connected devices + manager.requestGroupInfo(channel) { group -> + if (group != null) { + val ssid = group.networkName + val passphrase = group.passphrase + viewModel.updateLog("Group Info: SSID=$ssid, Passphrase=$passphrase") + viewModel.onDevicesChanged(group.clientList) + viewModel.updateLog("Connected Devices: ${group.clientList.size}") + } + } + } else { + Timber.tag(TAG).d("Connected as a client.") + // Inform ViewModel that the device is a client + CoroutineScope(Dispatchers.Main).launch { + viewModel.onGroupOwnerChanged(false) + } + } + } + } else { + // Disconnected from a P2P network + Timber.tag(TAG).d("Disconnected from group.") + viewModel.onDevicesChanged(emptyList()) + viewModel.updateLog("Disconnected from group.") + + // Inform ViewModel to handle disconnection + CoroutineScope(Dispatchers.Main).launch { + viewModel.onDisconnected() + } + } + } + + WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> { + // Optionally handle changes to this device's Wi-Fi state + Timber.tag(TAG).d("WIFI_P2P_THIS_DEVICE_CHANGED_ACTION received") + // Update ViewModel or log device state changes if necessary + } + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7b3f839..d09685f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,8 +11,13 @@ lifecycleRuntimeKtx = "2.8.6" activityCompose = "1.9.3" composeBom = "2024.04.01" composeMaterial = "1.4.0" +littleproxy = "2.0.0-beta6" mpandroidchart = "v3.1.0" +nanohttpd = "2.3.1" +nettyAll = "4.1.94.Final" +okhttp = "4.11.0" timber = "5.0.1" +datastorePreferencesCoreJvm = "1.1.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -32,8 +37,13 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "composeMaterial" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +littleproxy = { module = "org.littleshoot:littleproxy", version.ref = "littleproxy" } mpandroidchart = { module = "com.github.PhilJay:MPAndroidChart", version.ref = "mpandroidchart" } +nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" } +netty-all = { module = "io.netty:netty-all", version.ref = "nettyAll" } +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } +androidx-datastore-preferences-core-jvm = { group = "androidx.datastore", name = "datastore-preferences-core-jvm", version.ref = "datastorePreferencesCoreJvm" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }