diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9431f2f..9bbbb76 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + } android { @@ -45,6 +46,7 @@ android { packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" + resources.merges.add("META-INF/DEPENDENCIES") } } } @@ -60,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) @@ -69,6 +73,38 @@ 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") + + + // 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 ("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 ("androidx.core:core-ktx:1.12.0") // implementation ("androidx.activity:activity-compose:1.7.2") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0998f0b..d5f8d4d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ @@ -6,17 +7,50 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -24,6 +58,12 @@ - + + + + diff --git a/app/src/main/java/com/example/wifip2photspot/BandSelection.kt b/app/src/main/java/com/example/wifip2photspot/BandSelection.kt new file mode 100644 index 0000000..e317607 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/BandSelection.kt @@ -0,0 +1,58 @@ +package com.example.wifip2photspot + +import androidx.compose.foundation.clickable +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 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 new file mode 100644 index 0000000..e4d87b3 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/BlockedDevicesSection.kt @@ -0,0 +1,133 @@ +package com.example.wifip2photspot + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +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.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, + onUnblock: (String) -> Unit +) { + item { + Text( + text = "Blocked Devices (${devices.size}):", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + + if (devices.isEmpty()) { + item { + Text( + text = "No blocked devices.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + } else { + items(devices) { deviceInfo -> + BlockedDeviceCard( + deviceInfo = deviceInfo, + onUnblock = onUnblock + ) + Divider() + } + } +} + +@Composable +fun BlockedDeviceCard( + deviceInfo: DeviceInfo, + onUnblock: (String) -> Unit +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = deviceInfo.alias ?: "Unknown Device", + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = "MAC Address: ${deviceInfo.device.deviceAddress}", + style = MaterialTheme.typography.bodySmall + ) + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + OutlinedButton( + onClick = { onUnblock(deviceInfo.device.deviceAddress) } + ) { + Text("Unblock") + } + } + } + } +} diff --git a/app/src/main/java/com/example/wifip2photspot/ConnectedDevicesSection.kt b/app/src/main/java/com/example/wifip2photspot/ConnectedDevicesSection.kt new file mode 100644 index 0000000..5da7686 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ConnectedDevicesSection.kt @@ -0,0 +1,413 @@ +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.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.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.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, + onAliasChange: (String) -> Unit, + onBlockUnblock: (String) -> Unit, + onDisconnect: (String) -> Unit +) { + var isEditingAlias by remember { mutableStateOf(false) } + var aliasText by remember { mutableStateOf(deviceInfo.alias ?: "") } + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + colors = CardDefaults.cardColors( + containerColor = if (deviceInfo.isBlocked) MaterialTheme.colorScheme.errorContainer else MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + if (isEditingAlias) { + OutlinedTextField( + value = aliasText, + onValueChange = { aliasText = it }, + label = { Text("Alias") }, + singleLine = true, + trailingIcon = { + IconButton(onClick = { + onAliasChange(aliasText) + isEditingAlias = false + }) { + Icon(Icons.Default.Check, contentDescription = "Save Alias") + } + }, + modifier = Modifier.fillMaxWidth() + ) + } else { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = deviceInfo.alias ?: deviceInfo.device.deviceName, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.weight(1f) + ) + IconButton( + onClick = { isEditingAlias = true }, + modifier = Modifier.semantics { contentDescription = "Edit Alias" } + ) { + Icon(Icons.Default.Edit, contentDescription = "Edit Alias") + } + } + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "MAC Address: ${deviceInfo.device.deviceAddress}", + style = MaterialTheme.typography.bodySmall + ) + Text( + text = "Connected Since: ${formatTime(deviceInfo.connectionTime)}", + style = MaterialTheme.typography.bodySmall + ) + deviceInfo.ipAddress?.let { + Text("IP Address: $it", style = MaterialTheme.typography.bodySmall) + } + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + if (deviceInfo.isBlocked) { + OutlinedButton( + onClick = { onBlockUnblock(deviceInfo.device.deviceAddress) } + ) { + Text("Unblock") + } + } else { + OutlinedButton( + onClick = { onBlockUnblock(deviceInfo.device.deviceAddress) } + ) { + Text("Block") + } + Spacer(modifier = Modifier.width(8.dp)) + OutlinedButton( + onClick = { onDisconnect(deviceInfo.device.deviceAddress) } + ) { + Text("Disconnect") + } + } + } + } + } +} + +fun formatTime(timestamp: Long): String { + val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + 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, + onBlockUnblock: (String) -> Unit, + onDisconnect: (String) -> Unit +) { + item { + Text( + text = "Connected Devices (${devices.size}):", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + + if (devices.isEmpty()) { + item { + Text( + text = "No devices connected.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + } else { + items(devices) { deviceInfo -> + DeviceInfoCard( + deviceInfo = deviceInfo, + onAliasChange = { alias -> + onDeviceAliasChange(deviceInfo.device.deviceAddress, alias) + }, + 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") + } + } +} + +// 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 new file mode 100644 index 0000000..e180c38 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ConnectionStatusBar.kt @@ -0,0 +1,366 @@ +// 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.material.icons.Icons +import androidx.compose.material.icons.filled.DeviceHub +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.Upload +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.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( + uploadSpeed: Int, + downloadSpeed: Int, + totalDownload: Int, + connectedDevicesCount: Int, + errorMessage: String? = null +) { + // Main Card encapsulating all metrics + Card( + modifier = Modifier + .fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer + ), + shape = MaterialTheme.shapes.medium, + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Column( + modifier = Modifier + .padding(16.dp) + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + modifier = Modifier.fillMaxWidth() + ) { + // Speed Metrics Sub-Card + MetricSubCard( + title = stringResource(id = R.string.speed), + backgroundColor = MaterialTheme.colorScheme.surfaceVariant, + modifier = Modifier.weight(1f) + ) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + // Animated Upload Speed + AnimatedMetric( + icon = Icons.Default.Upload, + tint = Color(0xFF2196F3), // Blue + value = uploadSpeed, + unit = stringResource(id = R.string.kbps) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Animated Download Speed + AnimatedMetric( + icon = Icons.Default.Download, + tint = Color(0xFF4CAF50), // Green + value = downloadSpeed, + unit = stringResource(id = R.string.kbps) + ) + } + } + + Spacer(modifier = Modifier.width(8.dp)) + + // Total Download Metrics Sub-Card + MetricSubCard( + title = stringResource(id = R.string.download), + backgroundColor = MaterialTheme.colorScheme.surfaceVariant, + modifier = Modifier.weight(1f) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + imageVector = Icons.Default.Download, + contentDescription = null, + tint = Color(0xFFFF9800), // Orange + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + AnimatedText( + text = "$totalDownload", + unit = stringResource(id = R.string.kbps) + ) + } + } + + Spacer(modifier = Modifier.width(8.dp)) + + // Connected Devices Metrics Sub-Card + MetricSubCard( + title = stringResource(id = R.string.devices), + backgroundColor = MaterialTheme.colorScheme.surfaceVariant, + modifier = Modifier.weight(1f) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + imageVector = Icons.Default.DeviceHub, + contentDescription = null, + tint = Color(0xFF9C27B0), // Purple + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + AnimatedText( + text = "$connectedDevicesCount", + unit = "" + ) + } + } + } + + // Error Message Display + errorMessage?.let { message -> + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + } + } + } +} + +@Composable +fun AnimatedMetric( + icon: ImageVector, + tint: Color, + value: Int, + unit: String +) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, // Accessibility handled in parent composable + tint = tint, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + AnimatedText( + text = "$value", + unit = unit + ) + } +} + +@Composable +fun AnimatedText( + text: String, + unit: String +) { + // Animate the numeric value + val targetValue = text.toIntOrNull() ?: 0 + val animatedValue by animateIntAsState( + targetValue = targetValue, + animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing) + ) + + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "$animatedValue ", + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = unit, + style = MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurfaceVariant) + ) + } +} +@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 +fun MetricSubCard( + title: String, + backgroundColor: Color, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Card( + modifier = modifier, + colors = CardDefaults.cardColors( + containerColor = backgroundColor + ), + shape = MaterialTheme.shapes.medium, + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier + .padding(12.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = title, + style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(bottom = 8.dp) + ) + content() + } + } +} + +@Composable +fun DataUsageSection( + rxBytes: Long, + txBytes: Long +) { + Column(modifier = Modifier.padding(16.dp)) { + Text("Session Data Usage", 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" + } +} + +//@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 +) { + 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() + }, + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + ) +} +@Composable +fun BatteryStatusSection(batteryLevel: Int) { + Column(modifier = Modifier.padding(16.dp)) { + Text("Battery Status", style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + Text("Battery Level: $batteryLevel%") + } +} +//***********************improving************* +// +//if (_networkQuality.value == "Poor") { +// // Reduce video quality or data rate +//} else if (_networkQuality.value == "Moderate") { +// // Set to medium quality +//} else { +// // High quality +//} + + + + + diff --git a/app/src/main/java/com/example/wifip2photspot/DataStoreManager.kt b/app/src/main/java/com/example/wifip2photspot/DataStoreManager.kt new file mode 100644 index 0000000..6d822e9 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/DataStoreManager.kt @@ -0,0 +1,50 @@ +// 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 new file mode 100644 index 0000000..3d28f35 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/DataUsageRecord.kt @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000..c6fd6e5 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/DeviceInfo.kt @@ -0,0 +1,18 @@ +package com.example.wifip2photspot + +import android.net.wifi.p2p.WifiP2pDevice +import java.time.LocalDate + +// DeviceInfo.kt +data class DeviceInfo( + val device: WifiP2pDevice, + val alias: String? = null, + val connectionTime: Long = System.currentTimeMillis(), + val ipAddress: String? = null, + val isBlocked: Boolean = false +) + + + + + diff --git a/app/src/main/java/com/example/wifip2photspot/FeedbackForm.kt b/app/src/main/java/com/example/wifip2photspot/FeedbackForm.kt new file mode 100644 index 0000000..03e3e3d --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/FeedbackForm.kt @@ -0,0 +1,65 @@ +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.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material.ContentAlpha + +@Composable +fun FeedbackForm(onSubmit: (String) -> Unit) { + var feedbackText by remember { mutableStateOf("") } + + Column(modifier = Modifier.padding(16.dp)) { + Text("Feedback", style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = feedbackText, + onValueChange = { feedbackText = it }, + label = { Text("Your Feedback") }, + modifier = Modifier.fillMaxWidth(), + maxLines = 5 + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = { + onSubmit(feedbackText) + feedbackText = "" + }, + enabled = feedbackText.isNotBlank() + ) { + Text("Submit") + } + } +} + +@Composable +fun ContactSupportSection(onContactSupport: () -> Unit) { + Column(modifier = Modifier.padding(8.dp)) { + Text("Need Help?", style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + + Button(onClick = onContactSupport) { + Text("Contact Support") + } + } +} + + + diff --git a/app/src/main/java/com/example/wifip2photspot/HotspotControlSection.kt b/app/src/main/java/com/example/wifip2photspot/HotspotControlSection.kt new file mode 100644 index 0000000..63a34f9 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/HotspotControlSection.kt @@ -0,0 +1,133 @@ +// HotspotControlSection.kt +package com.example.wifip2photspot + +import android.content.Intent +import androidx.compose.foundation.layout.* +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.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, + passwordInput: String, + selectedBand: String, + onStartTapped: () -> Unit, + onStopTapped: () -> Unit, + proxyPort: Int, + + ) { + val context = LocalContext.current + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { +// Button( +// onClick = { +// onStartTapped() +// // Start ProxyService +// val intent = Intent(context, ProxyService::class.java) +// context.startService(intent) +// }, +// enabled = !isHotspotEnabled && !isProcessing +// ) + // Control Buttons + Spacer(modifier = Modifier.height(8.dp)) + + // Proxy Port Display (optional) + Text(text = "Proxy Port: $proxyPort") + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = { + if (isHotspotEnabled) { + onStopTapped() + } else { + onStartTapped() + } + }, + enabled = !isProcessing, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = if (isHotspotEnabled) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary + ) + ) + { + if (isProcessing) { + CircularProgressIndicator( + modifier = Modifier + .size(24.dp) + .padding(end = 8.dp), + color = MaterialTheme.colorScheme.onPrimary, + strokeWidth = 2.dp + ) + Text(text = if (isHotspotEnabled) "Stopping..." else "Starting...") + } else { + Text(text = if (isHotspotEnabled) "Stop WiFi-Direct" else "Start WiFi-Direct") + } + } + } +// { +// if (isProcessing) { +// CircularProgressIndicator( +// modifier = Modifier.size(20.dp), +// strokeWidth = 2.dp, +// color = MaterialTheme.colorScheme.onPrimary +// ) +// } else { +// Text("Start Hotspot & Proxy") +// } +// } + +// val statusIcon = when { +// isProcessing -> Icons.Default.Sync +// isHotspotEnabled -> Icons.Default.Wifi +// else -> Icons.Default.WifiOff +// } +// val statusText = when { +// isProcessing -> if (isHotspotEnabled) "Stopping hotspot..." else "Starting hotspot..." +// isHotspotEnabled -> "Hotspot & Proxy are active" +// else -> "Hotspot & Proxy are inactive" +// } +// Icon( +// imageVector = statusIcon, +// contentDescription = statusText, +// tint = if (isHotspotEnabled) Color(0xFF4CAF50) else Color(0xFFF44336) // Green or Red +// ) +// Button( +// onClick = { +// onStopTapped() +// // Stop ProxyService +// val intent = Intent(context, ProxyService::class.java) +// context.stopService(intent) +// }, +// enabled = isHotspotEnabled && !isProcessing +// ) { +// if (isProcessing) { +// CircularProgressIndicator( +// modifier = Modifier.size(20.dp), +// strokeWidth = 2.dp, +// color = MaterialTheme.colorScheme.onPrimary +// ) +// } else { +// Text("Stop Hotspot & Proxy") +// } +// } +} + + diff --git a/app/src/main/java/com/example/wifip2photspot/ImprovedHeader.kt b/app/src/main/java/com/example/wifip2photspot/ImprovedHeader.kt new file mode 100644 index 0000000..294ba40 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ImprovedHeader.kt @@ -0,0 +1,58 @@ +package com.example.wifip2photspot + + + +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.MoreVert +import com.example.wifip2photspot.viewModel.HotspotViewModel + +//@OptIn(ExperimentalMaterial3Api::class) +//@Composable +//fun ImprovedHeader(isHotspotEnabled: Boolean) { +// var showMenu by remember { mutableStateOf(false) } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ImprovedHeader( + isHotspotEnabled: Boolean, + HotspotViewModel: HotspotViewModel, + onSettingsClick: () -> Unit +) { + var showMenu by remember { mutableStateOf(false) } + + TopAppBar( + title = { Text("Asol") }, + navigationIcon = { + IconButton(onClick = { /* Open navigation drawer if needed */ }) { + Icon(Icons.Filled.Menu, contentDescription = "Menu") + } + }, + actions = { + 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 */ } + ) + // Other menu items as needed + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + titleContentColor = MaterialTheme.colorScheme.onPrimary, + actionIconContentColor = MaterialTheme.colorScheme.onPrimary + ) + ) +} diff --git a/app/src/main/java/com/example/wifip2photspot/InputFieldsSection.kt b/app/src/main/java/com/example/wifip2photspot/InputFieldsSection.kt new file mode 100644 index 0000000..2ca518c --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/InputFieldsSection.kt @@ -0,0 +1,212 @@ +// InputFieldsSection.kt +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 InputFieldsSection( + ssidInput: TextFieldValue, + onSsidChange: (TextFieldValue) -> Unit, + 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) } + + // Compute error states + val ssidErrorState = ssidInput.text.isEmpty() + val passwordErrorState = passwordInput.text.length !in 8..63 + if (!isHotspotEnabled) { + Card( + elevation = CardDefaults.cardElevation(4.dp), + shape = MaterialTheme.shapes.medium, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) { + Column( + modifier = Modifier + .padding(16.dp) + .imePadding() + ) { + // SSID Input Field + OutlinedTextField( + value = ssidInput, + onValueChange = onSsidChange, + label = { Text("SSID") }, + placeholder = { Text("TetherGuard") }, + leadingIcon = { + Icon( + Icons.Default.Wifi, + contentDescription = "SSID Icon" + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .alpha(if (!isHotspotEnabled) 1f else ContentAlpha.disabled), + enabled = !isHotspotEnabled, + singleLine = true, + isError = ssidErrorState, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Ascii, + imeAction = ImeAction.Next + ), + supportingText = { + if (ssidErrorState) { + Text( + text = "SSID cannot be empty", + color = MaterialTheme.colorScheme.error + ) + } + } + ) + + // Password Input Field with Show/Hide Toggle + OutlinedTextField( + value = passwordInput, + onValueChange = onPasswordChange, + label = { Text("Password") }, + placeholder = { Text("00000000") }, + leadingIcon = { + Icon( + Icons.Filled.Lock, + contentDescription = "Password Icon" + ) + }, + trailingIcon = { + val image = if (passwordVisible) + Icons.Filled.Visibility + else + Icons.Filled.VisibilityOff + + IconButton(onClick = { passwordVisible = !passwordVisible }) { + Icon( + imageVector = image, + contentDescription = if (passwordVisible) "Hide password" else "Show password" + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .alpha(if (!isHotspotEnabled) 1f else ContentAlpha.disabled), + enabled = !isHotspotEnabled, + singleLine = true, + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + isError = passwordErrorState, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + supportingText = { + if (passwordErrorState) { + Text( + text = "Password must be 8-63 characters", + color = MaterialTheme.colorScheme.error + ) + } + } + ) + + // 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) +// ) +// } + } + } + +// // 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/LogSection.kt b/app/src/main/java/com/example/wifip2photspot/LogSection.kt new file mode 100644 index 0000000..78303a9 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/LogSection.kt @@ -0,0 +1,55 @@ +package com.example.wifip2photspot + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun LogSection(logEntries: List) { + // Define the maximum number of log entries to display + val maxLogEntries = 100 + + // Remember only the last 'maxLogEntries' logs to avoid excessive display content + val truncatedLog = remember(logEntries) { + if (logEntries.size > maxLogEntries) { + logEntries.takeLast(maxLogEntries) + } else { + logEntries + } + } + + // Display logs in a Card with some styling and scrollable column + 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 = "Logs", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(bottom = 8.dp) + ) + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 100.dp, max = 200.dp) + .verticalScroll(rememberScrollState()) + ) { + // Display logs or fallback message + Text( + text = truncatedLog.joinToString("\n").ifEmpty { "No logs available." }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} diff --git a/app/src/main/java/com/example/wifip2photspot/MACAddressFilterSection.kt b/app/src/main/java/com/example/wifip2photspot/MACAddressFilterSection.kt new file mode 100644 index 0000000..996d43a --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/MACAddressFilterSection.kt @@ -0,0 +1,92 @@ +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 8b34ea4..50e35ff 100644 --- a/app/src/main/java/com/example/wifip2photspot/MainActivity.kt +++ b/app/src/main/java/com/example/wifip2photspot/MainActivity.kt @@ -1,828 +1,440 @@ +// MainActivity.kt package com.example.wifip2photspot -import android.Manifest -import android.annotation.SuppressLint +import android.app.Activity +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.BroadcastReceiver -import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.net.wifi.p2p.WifiP2pConfig +import android.net.VpnService import android.net.wifi.p2p.WifiP2pManager import android.os.Build import android.os.Bundle -import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.* -import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import androidx.core.app.ActivityCompat - -import android.net.wifi.p2p.WifiP2pDevice -import android.util.Log -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Block -import androidx.compose.material.icons.filled.Info -import androidx.compose.material.icons.filled.Lock -import androidx.compose.material.icons.filled.Menu -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material.icons.filled.Smartphone -import androidx.compose.material.icons.filled.Sync -import androidx.compose.material.icons.filled.Visibility -import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton +import androidx.compose.foundation.layout.fillMaxSize +import androidx.datastore.preferences.preferencesDataStore +import androidx.lifecycle.ViewModelProvider import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch -import androidx.compose.ui.Alignment -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.wear.compose.material.ContentAlpha -import androidx.compose.material.icons.filled.Wifi -import androidx.compose.material.icons.filled.WifiOff -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SwitchDefaults -import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBarDefaults +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.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.VisualTransformation -import kotlinx.coroutines.launch +import androidx.core.app.NotificationManagerCompat +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +//import com.example.wifip2photspot.Proxy.ProxyService +//import com.example.wifip2photspot.Proxy.ProxyService.Companion.CHANNEL_ID +import com.example.wifip2photspot.ui.theme.WiFiP2PHotspotTheme +import com.example.wifip2photspot.viewModel.HotspotViewModel +import com.example.wifip2photspot.viewModel.HotspotViewModelFactory +import com.example.wifip2photspot.viewModel.VpnViewModel +import com.example.wifip2photspot.viewModel.VpnViewModelFactory +import com.example.wifip2photspot.viewModel.WifiDirectBroadcastReceiver +import com.example.wifip2photspot.VPN.VpnRepository +import android.app.Application +import android.content.Context +//private val Context.dataStore by preferencesDataStore(name = "settings") class MainActivity : ComponentActivity() { + private lateinit var hotspotViewModel: HotspotViewModel + private lateinit var vpnViewModel: VpnViewModel + private val Context.dataStore: DataStore by preferencesDataStore(name = "settings") + +// // Define the permission launcher as a member variable +// private val requestPermissionLauncher = registerForActivityResult( +// ActivityResultContracts.RequestPermission() +// ) { isGranted: Boolean -> +// if (isGranted) { +// // Permission granted, proceed with notifications +// // You can send a confirmation toast or log if needed +// } else { +// // Permission denied, show alert to guide user +// showNotificationSettingsAlert.value = true +// } +// } +// // Mutable state to control the visibility of NotificationSettingsAlert +// private val showNotificationSettingsAlert = mutableStateOf(false) + - private var isWifiP2pEnabled = false +// private lateinit var viewModel: HotspotViewModel - // Make isHotspotEnabled observable by using mutable state - private var isHotspotEnabled by mutableStateOf(false) + private lateinit var receiver: WifiDirectBroadcastReceiver + private lateinit var intentFilter: IntentFilter - private lateinit var manager: WifiP2pManager + // private lateinit var receiver: BroadcastReceiver + private lateinit var wifiManager: WifiP2pManager private lateinit var channel: WifiP2pManager.Channel + private lateinit var notificationManager: NotificationManager - private lateinit var receiver: BroadcastReceiver - private lateinit var intentFilter: IntentFilter - private var isProcessing by mutableStateOf(false) + private val VPN_REQUEST_CODE = 1001 + private val channelName = "Data Usage Alerts" + private val channelDescription = "Notifications for data usage thresholds" + private val importance = NotificationManager.IMPORTANCE_HIGH - // Use MutableStateList to track connected devices - private val connectedDevices = mutableStateListOf() - private var updateLogCallback: ((String) -> Unit)? = null + @OptIn(ExperimentalComposeUiApi::class) @RequiresApi(Build.VERSION_CODES.Q) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // Request Notification Permission for Android 13+ - // Initialize Wi-Fi P2P manager and channel - manager = getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager - channel = manager.initialize(this, mainLooper, null) +// +// // Create notification channels +// createNotificationChannel() + + // Initialize Repository + val vpnRepository = VpnRepository(application) +// val dataStore = YourDataStoreType(this) // Initialize appropriately + + System.setProperty("user.home", filesDir.absolutePath) + +// Initialize VpnViewModel + val vpnViewModelFactory = VpnViewModelFactory(application, dataStore, vpnRepository) + vpnViewModel = ViewModelProvider(this, vpnViewModelFactory).get(VpnViewModel::class.java) + + // Initialize HotspotViewModel + val hotspotViewModelFactory = HotspotViewModelFactory(application, dataStore, vpnRepository) + hotspotViewModel = + ViewModelProvider(this, hotspotViewModelFactory).get(HotspotViewModel::class.java) + + wifiManager = + applicationContext.getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager + channel = wifiManager.initialize(this, mainLooper, null) + +// // Initialize NotificationManager +// notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager +// // Mutable state to control the visibility of NotificationSettingsAlert +// val showNotificationSettingsAlert = mutableStateOf(false) + + // Define the permission launcher as a member variable +// val requestPermissionLauncher = registerForActivityResult( +// ActivityResultContracts.RequestPermission() +// ) { isGranted: Boolean -> +// if (isGranted) { +// // Permission granted, proceed with notifications +// } else { +// // Permission denied, show alert to guide user +// showNotificationSettingsAlert.value = true +// } +// } + // Initialize the BroadcastReceiver + receiver = WifiDirectBroadcastReceiver( + manager = wifiManager, // Ensure wifiManager is initialized + channel = channel, // Ensure channel is initialized + viewModel = hotspotViewModel + ) - // Register the broadcast receiver + // Initialize IntentFilter intentFilter = IntentFilter().apply { addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION) + addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION) addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION) - // Add other actions if needed + addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION) } - receiver = WifiDirectBroadcastReceiver(manager, channel, this) - - // Request necessary permissions - ActivityCompat.requestPermissions( - this, arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.NEARBY_WIFI_DEVICES - ), 0 - ) - setContent { - WiFiP2PHotspotApp() +// // Initialize DataStore +// val dataStore = applicationContext.dataStore + + // Initialize ViewModel with Factory +// viewModel = ViewModelProvider( +// this, +// HotspotViewModelFactory(application, dataStore,vpnRepository) +// )[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 +// } +// // Register the channel with the system +// val notificationManager = +// getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager +// notificationManager.createNotificationChannel(channel) +// } + + + +// // Request necessary permissions +// if (!allPermissionsGranted()) { +// requestPermissions() +// } + +// setContent { +// // Theme State +// var isDarkTheme by rememberSaveable { mutableStateOf(false) } +// 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() +// } +// } +// } +// } +// } +// val viewModel = ViewModelProvider( +// this, +// HotspotViewModelFactory(application, dataStore,vpnRepository) +// ).get(HotspotViewModel::class.java) + // Start VPN service with user consent + val vpnIntent = VpnService.prepare(this) + if (vpnIntent != null) { + startActivityForResult(vpnIntent, VPN_REQUEST_CODE) + } else { + // VPN permissions already granted + vpnRepository.startVpn() } - } - override fun onResume() { - super.onResume() - registerReceiver(receiver, intentFilter) - } - - override fun onPause() { - super.onPause() - unregisterReceiver(receiver) - } - fun setIsWifiP2pEnabled(enabled: Boolean) { - isWifiP2pEnabled = enabled - } - @OptIn(ExperimentalMaterial3Api::class) - @RequiresApi(Build.VERSION_CODES.Q) - @Composable - fun WiFiP2PHotspotApp() { - var ssidInput by remember { mutableStateOf("") } - var passwordInput by remember { mutableStateOf("") } - var logMessages by remember { mutableStateOf("") } - var selectedBand by remember { mutableStateOf("Auto") } - var ssidError by remember { mutableStateOf(false) } - var passwordError by remember { mutableStateOf(false) } - var passwordVisible by remember { mutableStateOf(false) } - val snackbarHostState = remember { SnackbarHostState() } - val coroutineScope = rememberCoroutineScope() - var isProcessing by remember { mutableStateOf(false) } - - val mainActivity = LocalContext.current as MainActivity - val devices = mainActivity.connectedDevices - - val previousDeviceCount = remember { mutableIntStateOf(devices.size) } - - - val bands = listOf("Auto", "2.4GHz", "5GHz") - // Convert connectedDevices to a stable list for recomposition -// val devices by remember { derivedStateOf { connectedDevices.toList() } } - - Scaffold( - snackbarHost = { SnackbarHost(snackbarHostState) }, - - topBar = { ImprovedHeader() }, - content = { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .padding(horizontal = 16.dp) - .verticalScroll(rememberScrollState()) + setContent { + val isDarkTheme by hotspotViewModel.isDarkTheme.collectAsState() + WiFiP2PHotspotTheme(useDarkTheme = isDarkTheme) { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background ) { - // Input Fields and Hotspot Control Section - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) { - ssidError = ssidInput.isEmpty() - passwordError = passwordInput.length !in 8..63 - // Card with input fields - Card( - elevation = CardDefaults.cardElevation(4.dp), - shape = MaterialTheme.shapes.medium, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) { - Column(modifier = Modifier.padding(16.dp)) { - // SSID Input Field - OutlinedTextField( - value = ssidInput, - onValueChange = { ssidInput = it }, - label = { Text("SSID") }, - leadingIcon = { - Icon( - Icons.Default.Wifi, - contentDescription = null - ) - }, - modifier = Modifier - .fillMaxWidth() - .alpha(if (!isHotspotEnabled) 1f else ContentAlpha.disabled), - enabled = !isHotspotEnabled, - singleLine = true, - isError = ssidError, - supportingText = { - if (ssidError) { - Text( - text = "SSID cannot be empty", - color = MaterialTheme.colorScheme.error - ) - } - } - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Spacer(modifier = Modifier.height(8.dp)) - - // Password Input Field with Show/Hide Feature - OutlinedTextField( - value = passwordInput, - onValueChange = { passwordInput = it }, - label = { Text("Password") }, - leadingIcon = { - Icon( - Icons.Filled.Lock, - contentDescription = null - ) - }, - trailingIcon = { - val image = if (passwordVisible) - Icons.Filled.Visibility - else - Icons.Filled.VisibilityOff - - val description = - if (passwordVisible) "Hide password" else "Show password" - - IconButton(onClick = { - passwordVisible = !passwordVisible - }) { - Icon( - imageVector = image, - contentDescription = description - ) - } - }, - modifier = Modifier - .fillMaxWidth() - .alpha(if (!isHotspotEnabled) 1f else ContentAlpha.disabled), - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions( - onDone = { /* Handle action if needed */ } - ), - enabled = !isHotspotEnabled, - singleLine = true, - visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), - isError = passwordError, - supportingText = { - if (passwordError) { - Text( - text = "Password must be 8-63 characters", - color = MaterialTheme.colorScheme.error - ) - } - } - ) - } - } - // Band Selection using BandSelection Composable - BandSelection( - selectedBand = selectedBand, - onBandSelected = { selectedBand = it }, - bands = bands, - isHotspotEnabled = isHotspotEnabled - ) - // Inside your WiFiP2PHotspotApp Composable - HotspotControlSection( - isHotspotEnabled = isHotspotEnabled, - isProcessing = isProcessing, - ssidInput = ssidInput, - passwordInput = passwordInput, - selectedBand = selectedBand, - onStartTapped = { - onButtonStartTapped( - ssidInput, - passwordInput, - selectedBand - ) { message -> updateLog(message) } - }, - onStopTapped = { - onButtonStopTapped { message -> updateLog(message) } - } - ) - Spacer(modifier = Modifier.height(16.dp)) - - // Connected Devices Section - ConnectedDevicesSection( - devices = devices, - onDeviceClick = { device -> - // Handle device click - } - ) - - updateLogCallback = { message -> - logMessages += message - } - //logs - Spacer(modifier = Modifier.height(16.dp)) - LogSection(logMessages = logMessages) - } - // Observe changes in connectedDevices - LaunchedEffect(connectedDevices.size) { - if (connectedDevices.size > previousDeviceCount.value) { - // A device has connected - coroutineScope.launch { - snackbarHostState.showSnackbar("A device has connected.") - } - } else if (connectedDevices.size < previousDeviceCount.value) { - // A device has disconnected - coroutineScope.launch { - snackbarHostState.showSnackbar("A device has disconnected.") - } - } - previousDeviceCount.value = connectedDevices.size - } - } - - } - ) - } - - fun onDevicesChanged(deviceList: Collection) { - connectedDevices.clear() - connectedDevices.addAll(deviceList) - // Log for debugging - val deviceInfo = deviceList.joinToString(separator = "\n") { device -> - "Device Name: ${device.deviceName}, Address: ${device.deviceAddress}" - } - Log.d("ConnectedDevices", "Connected Devices:\n$deviceInfo") - } - - - private fun updateLog(message: String) { - updateLogCallback?.invoke(message) - } - - @RequiresApi(Build.VERSION_CODES.Q) - fun onButtonStartTapped( - ssidInput: String, - passwordInput: String, - selectedBand: String, - outputLog: (String) -> Unit - ) { - if (!isWifiP2pEnabled) { - outputLog("Error: Cannot start hotspot. Wi-Fi P2P is not enabled.\n") - Toast.makeText(this, "Wi-Fi P2P is not enabled.", Toast.LENGTH_SHORT).show() - // Reset the switch to off - isHotspotEnabled = false - return - } - isProcessing = true // Start processing - - - val ssidTrimmed = ssidInput.trim() - val passwordTrimmed = passwordInput.trim() - - if (ssidTrimmed.isEmpty()) { - outputLog("Error: SSID cannot be empty.\n") - Toast.makeText(this, "SSID cannot be empty.", Toast.LENGTH_SHORT).show() - // Reset the switch to off - isHotspotEnabled = false - return - } - - if (passwordTrimmed.length !in 8..63) { - outputLog("Error: The length of a passphrase must be between 8 and 63.\n") - Toast.makeText( - this, - "Password must be between 8 and 63 characters.", - Toast.LENGTH_SHORT - ).show() - // Reset the switch to off - isHotspotEnabled = false - return - } - - val ssid = "DIRECT-hs-$ssidTrimmed" - val password = passwordTrimmed - - 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(band) - .build() - - try { - manager.createGroup(channel, config, object : WifiP2pManager.ActionListener { - override fun onSuccess() { - outputLog("Hotspot started successfully.\n") - Toast.makeText( - this@MainActivity, - "Hotspot started successfully.", - Toast.LENGTH_SHORT - ).show() - isHotspotEnabled = true - outputLog("------------------- Hotspot Info -------------------\n") - outputLog("SSID: $ssid\n") - outputLog("Password: $password\n") - val bandStr = when (band) { - WifiP2pConfig.GROUP_OWNER_BAND_2GHZ -> "2.4GHz" - WifiP2pConfig.GROUP_OWNER_BAND_5GHZ -> "5GHz" - else -> "Auto" - } - outputLog("Band: $bandStr\n") - outputLog("---------------------------------------------------\n") - } - - 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" - } - outputLog("Failed to start hotspot. Reason: $reasonStr\n") - Toast.makeText( - this@MainActivity, - "Failed to start hotspot: $reasonStr", - Toast.LENGTH_SHORT - ).show() - // Reset the switch to off - isProcessing = false - isHotspotEnabled = false - } - }) - } catch (e: Exception) { - outputLog("Exception: ${e.message}\n") - Toast.makeText(this, "Exception occurred: ${e.message}", Toast.LENGTH_SHORT).show() - // Reset the switch to off - isProcessing = false - isHotspotEnabled = false - } - } - + val context = LocalContext.current + var showAlert by remember { mutableStateOf(false) } + // Set up NavHost + WiFiP2PHotspotApp( + hotspotViewModel = hotspotViewModel, + vpnViewModel = vpnViewModel + ) - fun onButtonStopTapped(outputLog: (String) -> Unit) { - if (!isHotspotEnabled) { - outputLog("Error: Hotspot is not enabled.\n") - Toast.makeText(this, "Hotspot is not enabled.", Toast.LENGTH_SHORT).show() - return - } - isProcessing = true // Start processing - - - try { - manager.removeGroup(channel, object : WifiP2pManager.ActionListener { - override fun onSuccess() { - outputLog("Hotspot stopped successfully.\n") - Toast.makeText( - this@MainActivity, - "Hotspot stopped successfully.", - Toast.LENGTH_SHORT - ).show() - isHotspotEnabled = false - // Clear the connected devices list - connectedDevices.clear() - } +// // Check if notifications are enabled +// LaunchedEffect(key1 = true) { +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { +// if (!NotificationManagerCompat.from(context).areNotificationsEnabled()) { +// // Notifications are disabled, show alert +// showNotificationSettingsAlert.value = true +// } else { +// // Notifications are enabled, request notification permission +// requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS +// ) +// } +// } else { +// // For devices below Android 13, no runtime permission required +// } +// } - 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" - } - outputLog("Failed to stop hotspot. Reason: $reasonStr\n") - Toast.makeText( - this@MainActivity, - "Failed to stop hotspot: $reasonStr", - Toast.LENGTH_SHORT - ).show() - // Set the switch back to on since we failed to stop the hotspot - isProcessing = false - isHotspotEnabled = true } - }) - } catch (e: Exception) { - outputLog("Exception: ${e.message}\n") - Toast.makeText(this, "Exception occurred: ${e.message}", Toast.LENGTH_SHORT).show() - // Set the switch back to on since we failed to stop the hotspot - isProcessing = false - isHotspotEnabled = true - e.printStackTrace() - } - } -} - -@Composable -fun ConnectedDevicesSection( - devices: List, - onDeviceClick: (WifiP2pDevice) -> Unit = {} -) { - Text( - text = "Connected Devices (${devices.size}):", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(vertical = 8.dp) - ) - - 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 - ) - } - } else { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 300.dp) - ) { - items(devices) { device -> - DeviceItem(device = device, onClick = onDeviceClick) } } } -} - - - -@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) } - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(12.dp) - ) { - Icon( - imageVector = Icons.Default.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 - ) - Text( - text = "Address: ${device.deviceAddress}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } -} - +// private fun createNotificationChannel() { +// // Notification channels are only available in Android 8.0 (API level 26) and above +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { +// val name = "Device Connection Notifications" +// val descriptionText = "Notifications related to device connections and VPN status." +// val importance = NotificationManager.IMPORTANCE_DEFAULT +// val channel = NotificationChannel("device_connection_channel", name, importance).apply { +// description = descriptionText +// } // -//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" +// // Register the channel with the system +// val notificationManager: NotificationManager = +// getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager +// notificationManager.createNotificationChannel(channel) +// } // } -//} +// override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { +// super.onActivityResult(requestCode, resultCode, data) +// if (requestCode == VPN_REQUEST_CODE && resultCode == Activity.RESULT_OK) { +// vpnRepository.startVpn() +// } +// } +// private fun startProxyService() { +// val intent = Intent(this, ProxyService::class.java) +// startService(intent) +// } +// +// private fun stopProxyService() { +// val intent = Intent(this, ProxyService::class.java) +// stopService(intent) +// } -@Composable -fun LogSection(logMessages: String) { - var isExpanded by remember { mutableStateOf(false) } - - Spacer(modifier = Modifier.height(16.dp)) - - TextButton(onClick = { isExpanded = !isExpanded }) { - Text(if (isExpanded) "Hide Logs" else "Show Logs") + override fun onDestroy() { + super.onDestroy() + // Ensure services are stopped } +// override fun onResume() { +// super.onResume() +// // Usage of receiver +// registerReceiver(receiver, IntentFilter) +// } - if (isExpanded) { - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 200.dp) - .background(MaterialTheme.colorScheme.surfaceVariant) - .padding(8.dp) - .verticalScroll(rememberScrollState()) - ) { - Text( - text = logMessages, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } + override fun onPause() { + super.onPause() + // Unregister receiver + unregisterReceiver(receiver) } -} - -@Composable -fun HotspotControlSection( - isHotspotEnabled: Boolean, - isProcessing: Boolean, - ssidInput: String, - passwordInput: String, - selectedBand: String, - onStartTapped: () -> Unit, - onStopTapped: () -> Unit -) { - Spacer(modifier = Modifier.height(16.dp)) - - // Hotspot Control Section - Card( - elevation = CardDefaults.cardElevation(4.dp), - shape = MaterialTheme.shapes.medium, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) { - Column( - modifier = Modifier.padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - // Status Text with Icon - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - val statusIcon = when { - isProcessing -> Icons.Default.Sync - isHotspotEnabled -> Icons.Default.Wifi - else -> Icons.Default.WifiOff - } - val statusText = when { - isProcessing -> if (isHotspotEnabled) "Stopping hotspot..." else "Starting hotspot..." - isHotspotEnabled -> "Hotspot is active" - else -> "Hotspot is inactive" - } - Icon( - imageVector = statusIcon, - contentDescription = statusText, - tint = if (isHotspotEnabled) Color(0xFF4CAF50) else Color(0xFFF44336) // Green or Red - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = statusText, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Start/Stop Button - Button( - onClick = { - if (isHotspotEnabled) { - onStopTapped() - } else { - onStartTapped() - } - }, - enabled = !isProcessing, - modifier = Modifier.fillMaxWidth() - ) { - if (isProcessing) { - CircularProgressIndicator( - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp, - modifier = Modifier.size(24.dp) - ) - } else { - Text(if (isHotspotEnabled) "Stop Hotspot" else "Start Hotspot") - } - } - } + override fun onResume() { + super.onResume() + registerReceiver(receiver, intentFilter) } -} - +// +// override fun onPause() { +// super.onPause() +// unregisterReceiver(receiver) +// } -@Composable -fun BandSelection( - selectedBand: String, - onBandSelected: (String) -> Unit, - bands: List, - isHotspotEnabled: Boolean -) { - Text( - text = "Select Band:", - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(top = 16.dp) - ) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.primary, - shape = RoundedCornerShape(8.dp) - ), - horizontalArrangement = Arrangement.SpaceBetween - ) { - bands.forEachIndexed { index, band -> - val isSelected = selectedBand == band - Box( - modifier = Modifier - .weight(1f) - .clickable( - enabled = !isHotspotEnabled, - onClick = { onBandSelected(band) } - ) - .background( - color = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent, - shape = if (index == 0) { - RoundedCornerShape(topStart = 8.dp, bottomStart = 8.dp) - } else if (index == bands.lastIndex) { - RoundedCornerShape(topEnd = 8.dp, bottomEnd = 8.dp) - } else { - RoundedCornerShape(0.dp) - } - ) - .padding(vertical = 12.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = band, - color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.bodySmall - ) - } - } - } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ImprovedHeader() { - var showMenu by remember { mutableStateOf(false) } - - TopAppBar( - title = { Text("Asol") }, - navigationIcon = { - IconButton(onClick = { /* Open navigation drawer */ }) { - Icon(Icons.Filled.Menu, contentDescription = "Menu") - } - }, - actions = { - IconButton(onClick = { /* Open settings */ }) { - 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 */ } - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primary, - titleContentColor = MaterialTheme.colorScheme.onPrimary, - actionIconContentColor = MaterialTheme.colorScheme.onPrimary - ), -// elevation = 4.dp - ) -} +// 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) +// } +//} diff --git a/app/src/main/java/com/example/wifip2photspot/NotificationSettingsSection.kt b/app/src/main/java/com/example/wifip2photspot/NotificationSettingsSection.kt new file mode 100644 index 0000000..8aa8144 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/NotificationSettingsSection.kt @@ -0,0 +1,90 @@ +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(8.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 +) { + var checkedState by rememberSaveable { mutableStateOf(true) } + Row( + modifier = Modifier + .fillMaxWidth() + .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/PermissionHandler.kt b/app/src/main/java/com/example/wifip2photspot/PermissionHandler.kt new file mode 100644 index 0000000..9d0e915 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/PermissionHandler.kt @@ -0,0 +1,136 @@ +// 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.material3.TextButton +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat +import androidx.lifecycle.compose.LocalLifecycleOwner + +@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() + } +} +// +//@Composable +//fun RequestPermissions(onPermissionsGranted: () -> Unit) { +// val context = LocalContext.current +// val lifecycleOwner = LocalLifecycleOwner.current +// +// val permissionState = rememberMultiplePermissionsState( +// permissions = listOf( +// android.Manifest.permission.ACCESS_FINE_LOCATION, +// android.Manifest.permission.CHANGE_WIFI_STATE, +// android.Manifest.permission.ACCESS_WIFI_STATE +// ) +// ) +// +// LaunchedEffect(key1 = true) { +// permissionState.launchMultiplePermissionRequest() +// } +// +// when { +// permissionState.allPermissionsGranted -> { +// onPermissionsGranted() +// } +// permissionState.shouldShowRationale -> { +// // Show rationale and request permission again +// AlertDialog( +// onDismissRequest = { /* Handle dismiss */ }, +// title = { Text("Permissions Required") }, +// text = { Text("This app requires location and Wi-Fi permissions to function correctly.") }, +// confirmButton = { +// TextButton(onClick = { permissionState.launchMultiplePermissionRequest() }) { +// Text("Grant") +// } +// }, +// dismissButton = { +// TextButton(onClick = { /* Handle dismiss */ }) { +// Text("Cancel") +// } +// } +// ) +// } +// else -> { +// // Permissions denied permanently +// AlertDialog( +// onDismissRequest = { /* Handle dismiss */ }, +// title = { Text("Permissions Denied") }, +// text = { Text("Please enable permissions from settings.") }, +// confirmButton = { +// TextButton(onClick = { /* Open app settings */ }) { +// Text("Settings") +// } +// }, +// dismissButton = { +// TextButton(onClick = { /* Handle dismiss */ }) { +// Text("Cancel") +// } +// } +// ) +// } +// } +//} diff --git a/app/src/main/java/com/example/wifip2photspot/Proxy/DataUsageSection.kt b/app/src/main/java/com/example/wifip2photspot/Proxy/DataUsageSection.kt new file mode 100644 index 0000000..9db1054 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/Proxy/DataUsageSection.kt @@ -0,0 +1,39 @@ +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 new file mode 100644 index 0000000..d5f3380 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyControlSection.kt @@ -0,0 +1,91 @@ +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 new file mode 100644 index 0000000..bdeb2bd --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyInstructionsSection.kt @@ -0,0 +1,179 @@ +// 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 new file mode 100644 index 0000000..c254ebb --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyLimitationsSection.kt @@ -0,0 +1,38 @@ +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 new file mode 100644 index 0000000..87b64c7 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyService.kt @@ -0,0 +1,457 @@ +// 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 new file mode 100644 index 0000000..e124903 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/Proxy/ProxyStatusSection.kt @@ -0,0 +1,39 @@ +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 new file mode 100644 index 0000000..1b11e19 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/StartHotspotWorker.kt @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000..a9d9c0d --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/StopHotspotWorker.kt @@ -0,0 +1,15 @@ +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/HotspotControlButton.kt b/app/src/main/java/com/example/wifip2photspot/VPN/HotspotControlButton.kt new file mode 100644 index 0000000..2e0893c --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/VPN/HotspotControlButton.kt @@ -0,0 +1,47 @@ +package com.example.wifip2photspot.VPN + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +// HotspotControlButton.kt +@Composable +fun HotspotControlButton( + isHotspotEnabled: Boolean, + isProcessing: Boolean, + onStartTapped: () -> Unit, + onStopTapped: () -> Unit +) { + Button( + onClick = { + if (isHotspotEnabled) { + onStopTapped() + } else { + onStartTapped() + } + }, + enabled = !isProcessing, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if (isHotspotEnabled) Color.Red else Color.Green + ) + ) { + if (isProcessing) { + CircularProgressIndicator( + modifier = Modifier + .size(24.dp) + .padding(end = 8.dp), + color = Color.White, + strokeWidth = 2.dp + ) + Text(text = if (isHotspotEnabled) "Stopping..." else "Starting...") + } else { + Text(text = if (isHotspotEnabled) "Stop Hotspot & VPN" else "Start Hotspot & VPN") + } + } +} diff --git a/app/src/main/java/com/example/wifip2photspot/VPN/MyVpnService.kt b/app/src/main/java/com/example/wifip2photspot/VPN/MyVpnService.kt new file mode 100644 index 0000000..099a69d --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/VPN/MyVpnService.kt @@ -0,0 +1,170 @@ +// MyVpnService.kt +package com.example.wifip2photspot.VPN + +import android.content.Intent +import android.net.VpnService +import android.os.ParcelFileDescriptor +import kotlinx.coroutines.* +import kotlinx.coroutines.NonCancellable.isActive +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.channels.DatagramChannel +import java.io.FileInputStream +import java.io.FileOutputStream + + +import kotlinx.coroutines.launch + +import java.net.Socket + +class MyVpnService : VpnService() { + + private var vpnInterface: ParcelFileDescriptor? = null + private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + establishVpn() + return START_STICKY + } + + private fun establishVpn() { + val builder = Builder() + .setSession("WiFiP2PHotspot VPN") + .addAddress("10.0.0.2", 24) // Virtual IP for Device A + .addRoute("0.0.0.0", 0) // Route all traffic through VPN + .addDnsServer("8.8.8.8") + .setMtu(1500) + + vpnInterface = builder.establish() + + if (vpnInterface != null) { + coroutineScope.launch { + try { + handleVpnTraffic(vpnInterface!!) + } catch (e: Exception) { + e.printStackTrace() + stopSelf() + } + } + } else { + // Handle failure to establish VPN + stopSelf() + } + } + + private suspend fun handleVpnTraffic(fd: ParcelFileDescriptor) { + val inputStream = FileInputStream(fd.fileDescriptor) + val outputStream = FileOutputStream(fd.fileDescriptor) + val buffer = ByteArray(32767) + + try { + while (isActive) { // isActive is available within CoroutineScope + val length = withContext(Dispatchers.IO) { + try { + inputStream.read(buffer) + } catch (e: IOException) { + e.printStackTrace() + -1 + } + } + + if (length > 0) { + // Extract destination IP and port from packet (requires parsing) + val destinationIp = extractDestinationIp(buffer, length) + val destinationPort = extractDestinationPort(buffer, length) + + // Forward packet to the destination server + val response = forwardPacketToServer(buffer, length, destinationIp, destinationPort) + + // Write the response back to the VPN interface + withContext(Dispatchers.IO) { + try { + outputStream.write(response, 0, response.size) + outputStream.flush() + } catch (e: IOException) { + e.printStackTrace() + } + } + } else if (length == -1) { + // End of stream or error + break + } + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + // Clean up streams + try { + inputStream.close() + } catch (e: IOException) { + e.printStackTrace() + } + try { + outputStream.close() + } catch (e: IOException) { + e.printStackTrace() + } + stopSelf() + } + } + + private fun extractDestinationIp(buffer: ByteArray, length: Int): String { + // Implement packet parsing to extract destination IP address + // Placeholder implementation + return "8.8.8.8" + } + + private fun extractDestinationPort(buffer: ByteArray, length: Int): Int { + // Implement packet parsing to extract destination port + // Placeholder implementation + return 53 + } + + private fun forwardPacketToServer( + buffer: ByteArray, + length: Int, + destinationIp: String, + destinationPort: Int + ): ByteArray { + // Implement logic to forward the packet to the destination server + // For example, create a socket connection and send the data + + return try { + val socket = Socket(destinationIp, destinationPort) + val output = socket.getOutputStream() + val input = socket.getInputStream() + + output.write(buffer, 0, length) + output.flush() + + val responseBuffer = ByteArray(32767) + val responseLength = input.read(responseBuffer) + + socket.close() + + if (responseLength > 0) { + responseBuffer.copyOf(responseLength) + } else { + ByteArray(0) + } + } catch (e: IOException) { + e.printStackTrace() + ByteArray(0) + } + } + + override fun onDestroy() { + super.onDestroy() + try { + vpnInterface?.close() + } catch (e: IOException) { + e.printStackTrace() + } + coroutineScope.cancel() + } +} + + + + + diff --git a/app/src/main/java/com/example/wifip2photspot/VPN/VpnControlSection.kt b/app/src/main/java/com/example/wifip2photspot/VPN/VpnControlSection.kt new file mode 100644 index 0000000..c50e6b9 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/VPN/VpnControlSection.kt @@ -0,0 +1,124 @@ +package com.example.wifip2photspot.VPN + +import androidx.compose.foundation.background +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.CheckCircle +import androidx.compose.material.icons.filled.Error +import androidx.compose.material3.Button +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.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 + +@Composable +fun VpnSettingsSection( + isVpnActive: Boolean, + vpnStatusMessage: String, + onVpnToggle: (Boolean) -> Unit +) { + Column(modifier = Modifier.padding(8.dp)) { + Text("VPN Settings", style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = "Enable VPN Tethering", style = MaterialTheme.typography.bodyLarge) + Spacer(modifier = Modifier.weight(1f)) + Switch( + checked = isVpnActive, + onCheckedChange = onVpnToggle + ) + } + Text( + text = vpnStatusMessage, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(top = 4.dp) + ) + } +} + +@Composable +// VpnStatusDisplay.kt +fun VpnStatusDisplay( + isVpnActive: Boolean, + vpnStatusMessage: String +) { + Row( + modifier = Modifier + .fillMaxWidth() + .background( + if (isVpnActive) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) + else MaterialTheme.colorScheme.error.copy(alpha = 0.1f) + ) + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = if (isVpnActive) Icons.Filled.CheckCircle else Icons.Filled.Error, + contentDescription = "VPN Status", + tint = if (isVpnActive) Color.Green else Color.Red + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = if (isVpnActive) "VPN is active" else "VPN is inactive", + style = MaterialTheme.typography.bodyLarge, + color = if (isVpnActive) Color.Green else Color.Red + ) + } +} + + +// 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/VPN/VpnRepository.kt b/app/src/main/java/com/example/wifip2photspot/VPN/VpnRepository.kt new file mode 100644 index 0000000..52a2883 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/VPN/VpnRepository.kt @@ -0,0 +1,33 @@ +package com.example.wifip2photspot.VPN + +import android.app.Application +import android.content.Intent +// VpnRepository.kt (Extended) + + +import android.content.Context + +class VpnRepository(private val context: Context) { + + private var isVpnActive: Boolean = false + + fun startVpn() { + if (!isVpnActive) { + val vpnIntent = Intent(context, MyVpnService::class.java) + context.startService(vpnIntent) + isVpnActive = true + } + } + + fun stopVpn() { + if (isVpnActive) { + val vpnIntent = Intent(context, MyVpnService::class.java) + context.stopService(vpnIntent) + isVpnActive = false + } + } + + fun isVpnActive(): Boolean { + return isVpnActive + } +} 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 e887171..0000000 --- a/app/src/main/java/com/example/wifip2photspot/WiFiDirectBroadcastReceiver.kt +++ /dev/null @@ -1,52 +0,0 @@ -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.WifiP2pDevice -import android.net.wifi.p2p.WifiP2pManager - -class WifiDirectBroadcastReceiver( - private val manager: WifiP2pManager, - private val channel: WifiP2pManager.Channel, - private val activity: MainActivity -) : BroadcastReceiver() { - - override fun onReceive(context: Context, intent: Intent) { - val action = intent.action - - when (action) { - WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> { - // Check to see if Wi-Fi is enabled and notify appropriate activity - val state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1) - if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) { - activity.setIsWifiP2pEnabled(true) - } else { - activity.setIsWifiP2pEnabled(false) - } - } - 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) { - // We are connected with the other device, request connection info to find group owner IP - manager.requestConnectionInfo(channel) { info -> - // Use info.groupOwnerAddress to get the IP address - // You can also get group info to get the list of connected devices - manager.requestGroupInfo(channel) { group -> - if (group != null) { - val deviceList = group.clientList - activity.onDevicesChanged(deviceList) - } - } - } - } else { - // Disconnected - activity.onDevicesChanged(emptyList()) - } - } - // 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 new file mode 100644 index 0000000..d8b86a5 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/WiFiP2PHotspotApp.kt @@ -0,0 +1,454 @@ +// WiFiP2PHotspotApp.kt +package com.example.wifip2photspot + + +import android.content.Context +import android.content.Intent +import android.location.LocationManager +import android.net.Uri +import android.net.wifi.WifiManager +import android.provider.Settings +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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 +import com.example.wifip2photspot.viewModel.VpnViewModel + +@Composable +fun WiFiP2PHotspotApp( + hotspotViewModel: HotspotViewModel, + vpnViewModel: VpnViewModel +) { + val navController = rememberNavController() + WiFiP2PHotspotNavHost( + navController = navController, + hotspotViewModel = hotspotViewModel, + vpnViewModel = vpnViewModel + ) +} + +@Composable +fun WiFiP2PHotspotNavHost( + navController: NavHostController, + hotspotViewModel: HotspotViewModel, + vpnViewModel: VpnViewModel +) { + NavHost(navController = navController, startDestination = "main_screen") { + composable("main_screen") { + MainScreen( + navController = navController, + hotspotViewModel = hotspotViewModel, + vpnViewModel = vpnViewModel + ) + } + composable("settings_screen") { + SettingsScreen( + navController = navController, + hotspotViewModel = hotspotViewModel, + vpnViewModel = vpnViewModel + ) + } + } +} +// +//@SuppressLint("StateFlowValueCalledInComposition") +//@Composable +//fun MainScreen(navController: NavHostController, viewModel: HotspotViewModel) { +// val context = LocalContext.current +// 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() +// val batteryLevel by viewModel.batteryLevel.collectAsState() +// val remainingIdleTime by viewModel.remainingIdleTime.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" +// ) +// } +// // Start idle monitoring when the hotspot is enabled +// LaunchedEffect(isHotspotEnabled) { +// if (isHotspotEnabled) { +// viewModel.startIdleMonitoring() +// } +// } +// // 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) +// } +// } +// is HotspotViewModel.UiEvent.StopProxyService -> { +// val intent = Intent(context, ProxyService::class.java) +// context.stopService(intent) +// } +// } +// } +// } +// // Scaffold for overall layout +// Scaffold( +// topBar = { +// ImprovedHeader( +// isHotspotEnabled = isHotspotEnabled, +// viewModel = viewModel, +// 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)) +// } +// +// if (connectedDeviceInfos.isNotEmpty()) { +// item { +// SpeedGraphSection( +// uploadSpeeds = uploadSpeedEntries, +// downloadSpeeds = downloadSpeedEntries +// ) +// } +// } +// +// // Input Fields and Band Selection +// if (connectedDeviceInfos.isEmpty()) { +// item { +// InputFieldsSection( +// ssidInput = ssidFieldState, +// onSsidChange = { newValue -> +// ssidFieldState = newValue +// }, +// passwordInput = passwordFieldState, +// onPasswordChange = { newValue -> +// passwordFieldState = newValue +// }, +// isHotspotEnabled = isHotspotEnabled +// ) +// } +// } else { +// 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, +// 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)) +// } +// 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) +// } +// +// } +// } +// ) +//} +// + +/** + * 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) +} + +/** + * Determines if Wi-Fi is enabled. + */ +fun isWifiEnabled(context: Context): Boolean { + val wifiManager = + context.applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager + return wifiManager?.isWifiEnabled ?: false +} + +/** + * Determines if Location services are enabled. + */ +fun isLocationEnabled(context: Context): Boolean { + val locationManager = + context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager + return locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER) == true || locationManager?.isProviderEnabled( + LocationManager.NETWORK_PROVIDER + ) == 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/permissions/RequestVpnPermission.kt b/app/src/main/java/com/example/wifip2photspot/permissions/RequestVpnPermission.kt new file mode 100644 index 0000000..dfdcde6 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/permissions/RequestVpnPermission.kt @@ -0,0 +1,54 @@ +package com.example.wifip2photspot.permissions + +import android.net.VpnService +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import com.example.wifip2photspot.viewModel.VpnViewModel + +// RequestVpnPermission.kt + +@Composable +fun RequestVpnPermission(vpnViewModel: VpnViewModel) { + val context = LocalContext.current + val vpnIntent = remember { VpnService.prepare(context) } + var showDialog by remember { mutableStateOf(false) } + + LaunchedEffect(vpnIntent) { + if (vpnIntent != null) { + // Show a dialog explaining why VPN permission is needed + showDialog = true + } else { + // Permission already granted + vpnViewModel.toggleVpn(true) + } + } + + if (showDialog) { + AlertDialog( + onDismissRequest = { /* Handle dismissal */ }, + title = { Text("Allow VPN Connection") }, + text = { Text("This app requires VPN permission to route internet traffic through the hotspot securely.") }, + confirmButton = { + TextButton(onClick = { + context.startActivity(vpnIntent) + showDialog = false + }) { + Text("Allow") + } + }, + dismissButton = { + TextButton(onClick = { showDialog = false }) { + Text("Deny") + } + } + ) + } +} diff --git a/app/src/main/java/com/example/wifip2photspot/socksProxy/SSHServerManager.kt b/app/src/main/java/com/example/wifip2photspot/socksProxy/SSHServerManager.kt new file mode 100644 index 0000000..d999845 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/socksProxy/SSHServerManager.kt @@ -0,0 +1,86 @@ +package com.example.wifip2photspot.socksProxy + + +// SSHServerManager.kt + +import android.content.Context +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.apache.sshd.common.keyprovider.KeyPairProvider +import org.apache.sshd.server.SshServer +import org.apache.sshd.server.auth.password.PasswordAuthenticator +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider +import org.apache.sshd.server.session.ServerSession +import java.io.IOException +import java.net.InetSocketAddress +import java.nio.file.Paths + +class SSHServerManager( + private val context: Context, + private val sshUsername: String, + private val sshPassword: String, + private val proxyPort: Int = 8181 // SOCKS Proxy Port +) { + + private var sshServer: SshServer? = null + private val TAG = "SSHServerManager" + + suspend fun startSSHServer(localPort: Int = 2222) { + withContext(Dispatchers.IO) { + // Initialize the SSH server after 'user.home' is set + sshServer = SshServer.setUpDefaultServer() + sshServer?.port = localPort + + // Host Key Provider expects a Path, so we convert the string to a Path + val hostKeyPath = Paths.get(context.filesDir.absolutePath, "hostkey.ser") + val hostKeyProvider: KeyPairProvider = SimpleGeneratorHostKeyProvider(hostKeyPath) + sshServer?.keyPairProvider = hostKeyProvider + + // Set up Password Authentication + sshServer?.passwordAuthenticator = + PasswordAuthenticator { username, password, session -> + val clientAddress = session.clientAddress + val ipAddress = if (clientAddress is InetSocketAddress) { + clientAddress.address.hostAddress + } else { + null + } + // Allow only connections from the local Wi-Fi Direct IP range (e.g., 192.168.49.x) + val isAllowedIP = ipAddress?.startsWith("192.168.49.") ?: false + val isAuthenticated = username == sshUsername && password == sshPassword + Log.d(TAG, "Authentication attempt from $ipAddress: ${if (isAuthenticated && isAllowedIP) "Success" else "Failure"}") + isAuthenticated && isAllowedIP + } + + try { + sshServer?.start() + Log.d(TAG, "SSH Server started on port $localPort") + } catch (e: IOException) { + Log.e(TAG, "Failed to start SSH Server: ${e.message}") + e.printStackTrace() + } + + } + } + + + suspend fun stopSSHServer() { + withContext(Dispatchers.IO) { + try { + sshServer?.stop() + sshServer = null + Log.d(TAG, "SSH Server stopped.") + } catch (e: IOException) { + Log.e(TAG, "Failed to stop SSH Server: ${e.message}") + e.printStackTrace() + } + } + } + + fun isRunning(): Boolean { + return sshServer?.isOpen ?: false + } +} + + diff --git a/app/src/main/java/com/example/wifip2photspot/socksProxy/SSHTunnelManager.kt b/app/src/main/java/com/example/wifip2photspot/socksProxy/SSHTunnelManager.kt new file mode 100644 index 0000000..1154f75 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/socksProxy/SSHTunnelManager.kt @@ -0,0 +1,45 @@ +package com.example.wifip2photspot.socksProxy + + +import com.jcraft.jsch.JSch +import com.jcraft.jsch.Session +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class SSHTunnelManager( + private val username: String, + private val password: String, + private val host: String, + private val port: Int = 22, + private val remotePort: Int = 80, // HTTP + private val localPort: Int = 1080 // SOCKS Proxy Port +) { + private var session: Session? = null + + suspend fun connect() { + withContext(Dispatchers.IO) { + val jsch = JSch() + session = jsch.getSession(username, host, port) + session?.setPassword(password) + + // Avoid asking for key confirmation + session?.setConfig("StrictHostKeyChecking", "no") + + session?.connect() + + // Set up dynamic port forwarding (SOCKS) + session?.setPortForwardingL(localPort, "localhost", remotePort) + } + } + + suspend fun disconnect() { + withContext(Dispatchers.IO) { + session?.disconnect() + session = null + } + } + + fun isConnected(): Boolean { + return session?.isConnected ?: false + } +} 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..9dd0211 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ui/IdleCountdownDisplay.kt @@ -0,0 +1,79 @@ +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.graphics.Color +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/MyApplication.kt b/app/src/main/java/com/example/wifip2photspot/ui/MyApplication.kt new file mode 100644 index 0000000..8b299ce --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ui/MyApplication.kt @@ -0,0 +1,69 @@ +package com.example.wifip2photspot.ui + +import android.app.Application +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Intent +import android.os.Build +import android.provider.Settings +import androidx.compose.runtime.Composable +import androidx.compose.material3.* +import androidx.compose.ui.platform.LocalContext + +import android.content.Context + + +class MyApplication : Application() { + + override fun onCreate() { + super.onCreate() + createNotificationChannel() + } + + private fun createNotificationChannel() { + // Notification channels are only available in Android 8.0 (API level 26) and above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = "Device Connection Notifications" + val descriptionText = "Notifications related to device connections and VPN status." + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel("device_connection_channel", name, importance).apply { + description = descriptionText + } + + // Register the channel with the system + val notificationManager: NotificationManager = + getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + } +} + + +@Composable +fun NotificationSettingsAlert(showAlert: Boolean, onDismiss: () -> Unit) { + if (showAlert) { + val context = LocalContext.current + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Enable Notifications") }, + text = { Text("Please enable notifications in your device settings to stay updated on device connections.") }, + confirmButton = { + TextButton(onClick = { + // Open app notification settings + val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } + context.startActivity(intent) + onDismiss() + }) { + Text("Settings") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) + } +} 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..60a43ce --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ui/SettingsContent.kt @@ -0,0 +1,129 @@ +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.NotificationSettingsSection +import com.example.wifip2photspot.VPN.VpnSettingsSection +import com.example.wifip2photspot.ui.theme.ThemeToggle +import com.example.wifip2photspot.viewModel.VpnViewModel + +@Composable +fun SettingsContent( + hotspotViewModel: HotspotViewModel, + vpnViewModel: VpnViewModel, + paddingValues: PaddingValues +) { + // Collect necessary state from ViewModel + val isDarkTheme by hotspotViewModel.isDarkTheme.collectAsState() + val notificationEnabled by hotspotViewModel.notificationEnabled.collectAsState() + val soundEnabled by hotspotViewModel.notificationSoundEnabled.collectAsState() + val vibrationEnabled by hotspotViewModel.notificationVibrationEnabled.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() + + val isVpnActive by vpnViewModel.isVpnActive.collectAsState() + val vpnStatusMessage by vpnViewModel.vpnStatusMessage.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)) + + // Notification Settings + NotificationSettingsSection( + notificationEnabled = notificationEnabled, + onNotificationEnabledChange = { hotspotViewModel.updateNotificationEnabled(it) }, + soundEnabled = soundEnabled, + onSoundEnabledChange = { hotspotViewModel.updateNotificationSoundEnabled(it) }, + vibrationEnabled = vibrationEnabled, + onVibrationEnabledChange = { hotspotViewModel.updateNotificationVibrationEnabled(it) } + ) + + 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)) + + // VPN Settings Section + VpnSettingsSection( + isVpnActive = isVpnActive, + vpnStatusMessage = vpnStatusMessage, + onVpnToggle = { enabled -> + vpnViewModel.toggleVpn(enabled) + } + ) + + 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)) + + // Add more settings sections as needed... + } +} \ 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..33b313d --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ui/SettingsScreen.kt @@ -0,0 +1,40 @@ +package com.example.wifip2photspot.ui + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.navigation.NavHostController +import com.example.wifip2photspot.viewModel.HotspotViewModel +import com.example.wifip2photspot.viewModel.VpnViewModel + +@OptIn(ExperimentalMaterial3Api::class) + +@Composable +fun SettingsScreen( + navController: NavHostController, + hotspotViewModel: HotspotViewModel, + vpnViewModel: VpnViewModel +){ + Scaffold( + topBar = { + TopAppBar( + title = { Text("Settings") }, + navigationIcon = { + IconButton(onClick = { navController.navigateUp() }) { + Icon(Icons.Filled.ArrowBack, contentDescription = "Back") + } + } + ) + }, + content = { paddingValues -> + SettingsContent( + hotspotViewModel = hotspotViewModel, + vpnViewModel = vpnViewModel, + 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..4c03db7 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ui/WifiLockSettingsSection.kt @@ -0,0 +1,35 @@ +package com.example.wifip2photspot.ui + +import androidx.compose.foundation.layout.* + +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 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..b27142d --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ui/screens/MainScreen.kt @@ -0,0 +1,365 @@ +package com.example.wifip2photspot.ui.screens + + +import android.Manifest +import android.annotation.SuppressLint +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +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.input.nestedscroll.NestedScrollSource.Companion.SideEffect +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat.getSystemService +import androidx.navigation.NavHostController +import com.example.wifip2photspot.BatteryStatusSection +import com.example.wifip2photspot.ConnectionStatusBar +import com.example.wifip2photspot.HotspotControlSection +import com.example.wifip2photspot.viewModel.HotspotViewModel +import com.example.wifip2photspot.ImprovedHeader +import com.example.wifip2photspot.InputFieldsSection +import com.example.wifip2photspot.LogSection +//import com.example.wifip2photspot.Proxy.ProxyService +import com.example.wifip2photspot.SpeedGraphSection +import com.example.wifip2photspot.VPN.VpnStatusDisplay +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.VPN.HotspotControlButton +import com.example.wifip2photspot.socksProxy.ProxyInfoDisplay +import com.example.wifip2photspot.socksProxy.ProxySetupInstructions +import com.example.wifip2photspot.ui.NotificationSettingsAlert +import com.example.wifip2photspot.viewModel.VpnViewModel + +@SuppressLint("StateFlowValueCalledInComposition", "UnrememberedMutableState") +@Composable +fun MainScreen( + navController: NavHostController, + hotspotViewModel: HotspotViewModel, + vpnViewModel: VpnViewModel +) { + 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 uploadSpeed by hotspotViewModel.uploadSpeed.collectAsState() + val downloadSpeed by hotspotViewModel.downloadSpeed.collectAsState() + val connectedDevices by hotspotViewModel.connectedDevices.collectAsState() + val logEntries by hotspotViewModel.logEntries.collectAsState() + val remainingIdleTime by hotspotViewModel.remainingIdleTime.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 hotspotViewModel.connectedDeviceInfos.collectAsState() + // Collect the blocked devices from the hotspotViewModel + val blockedDeviceInfos by hotspotViewModel.blockedDeviceInfos.collectAsState() + val (sessionRxBytes, sessionTxBytes) = hotspotViewModel.getSessionDataUsage() + val uploadSpeedEntries by hotspotViewModel.uploadSpeedEntries.collectAsState() + val downloadSpeedEntries by hotspotViewModel.downloadSpeedEntries.collectAsState() + val batteryLevel by hotspotViewModel.batteryLevel.collectAsState() + // Proxy server state +// val proxyPort by hotspotViewModel.proxyPort.collectAsState() +// // Mutable state to control the visibility of NotificationSettingsAlert +// val showNotificationSettingsAlert = mutableStateOf(false) + + val isVpnActive by vpnViewModel.isVpnActive.collectAsState() + val vpnStatusMessage by vpnViewModel.vpnStatusMessage.collectAsState() + + val proxyPort = 8181 // Must match the proxy and SSH local port + + + + // Update hotspotViewModel when text changes + LaunchedEffect(ssidFieldState.text) { + hotspotViewModel.updateSSID(ssidFieldState.text) + } + LaunchedEffect(passwordFieldState.text) { + hotspotViewModel.updatePassword(passwordFieldState.text) + } + LaunchedEffect(connectedDeviceInfos) { + Log.d( + "WiFiP2PHotspotApp", + "ConnectedDeviceInfos updated: ${connectedDeviceInfos.size} devices" + ) + } + // Start idle monitoring when the hotspot is enabled + LaunchedEffect(isHotspotEnabled) { + if (isHotspotEnabled) { + hotspotViewModel.startIdleMonitoring() + } + } + // Handle UI Events + LaunchedEffect(key1 = true) { + hotspotViewModel.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) +// } +// } + +// is HotspotViewModel.UiEvent.StopProxyService -> { +// val intent = Intent(context, ProxyService::class.java) +// context.stopService(intent) +// } + HotspotViewModel.UiEvent.StartProxyService -> TODO() + HotspotViewModel.UiEvent.StopProxyService -> TODO() + } + } + } + // Scaffold for overall layout + Scaffold( + topBar = { + ImprovedHeader( + isHotspotEnabled = isHotspotEnabled, + HotspotViewModel = hotspotViewModel, + onSettingsClick = { navController.navigate("settings_screen") }, + ) + }, + content = { paddingValues -> + LazyColumn( + contentPadding = paddingValues, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + item { + Spacer(modifier = Modifier.height(16.dp)) + } + + item { + // Proxy Information Display + ProxyInfoDisplay( + proxyIp = "192.168.49.1", + proxyPort = 8181 + ) + } + + item { + // VPN Status Display + VpnStatusDisplay( + isVpnActive = vpnViewModel.isVpnActive.collectAsState().value, + vpnStatusMessage = vpnViewModel.vpnStatusMessage.collectAsState().value + ) + } + + // Display Idle Countdown if applicable + item { + IdleCountdownDisplay(remainingIdleTime = remainingIdleTime) + + } + item { + Spacer(modifier = Modifier.height(16.dp)) + } + + if (connectedDeviceInfos.isNotEmpty()) { + item { + SpeedGraphSection( + uploadSpeeds = uploadSpeedEntries, + downloadSpeeds = downloadSpeedEntries + ) + } + item { + Spacer(modifier = Modifier.height(16.dp)) + // Proxy Setup Instructions + ProxySetupInstructions() + + Spacer(modifier = Modifier.height(16.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 + ) + } + } else { + item { + ConnectionStatusBar( + uploadSpeed = uploadSpeed, + downloadSpeed = downloadSpeed, + totalDownload = downloadSpeed, // Adjust if you have a separate totalDownload + connectedDevicesCount = connectedDevices.size + ) + } + } +// item { +// // VPN Status Display +// VpnStatusDisplay( +// isVpnActive = isVpnActive, +// vpnStatusMessage = vpnStatusMessage +// ) +// } +// item { +// // Hotspot Control Button +// HotspotControlButton( +// isHotspotEnabled = isHotspotEnabled, +// isProcessing = isProcessing, +// onStartTapped = { hotspotViewModel.startTethering() }, +// onStopTapped = { hotspotViewModel.stopTethering() } +// ) +// } + + 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, + ssidInput = ssidFieldState.text, + passwordInput = passwordFieldState.text, + selectedBand = selectedBand, + proxyPort = proxyPort, + onStartTapped = { + hotspotViewModel.startTethering() +// hotspotViewModel.on( +// 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) + } + + } +// // Show the Notification Settings Alert if needed +// NotificationSettingsAlert( +// showAlert = showNotificationSettingsAlert.value, +// onDismiss = { showNotificationSettingsAlert.value = false } +// ) +// SSH Configuration Dialog +// SSHConfigurationDialog( +// isOpen = showSSHConfigDialog.value, +// onDismiss = { showSSHConfigDialog.value = false }, +// onSave = { username, password, host, port -> +// // Save credentials securely +// hotspotViewModel.saveSSHCredentials(username, password) +// hotspotViewModel.setSSHHost(host) +// hotspotViewModel.setSSHPort(port) +// } +// ) + } + ) +} + diff --git a/app/src/main/java/com/example/wifip2photspot/ui/theme/Theme.kt b/app/src/main/java/com/example/wifip2photspot/ui/theme/Theme.kt index abdb00a..28e9e98 100644 --- a/app/src/main/java/com/example/wifip2photspot/ui/theme/Theme.kt +++ b/app/src/main/java/com/example/wifip2photspot/ui/theme/Theme.kt @@ -1,58 +1,41 @@ +// Theme.kt package com.example.wifip2photspot.ui.theme -import android.app.Activity -import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.* import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.graphics.Color -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 +private val LightColors = lightColorScheme( + primary = Color(0xFF6200EE), + onPrimary = Color.White, + surface = Color.White, + onSurface = Color.Black, + error = Color(0xFFB00020), + onError = Color.White, + // Add other colors as needed ) -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ +private val DarkColors = darkColorScheme( + primary = Color(0xFFBB86FC), + onPrimary = Color.Black, + surface = Color(0xFF121212), + onSurface = Color.White, + error = Color(0xFFCF6679), + onError = Color.Black, + // Add other colors as needed ) @Composable -fun Wifip2photspotTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, +fun WiFiP2PHotspotTheme( + useDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - darkTheme -> DarkColorScheme - else -> LightColorScheme - } + val colors = if (useDarkTheme) DarkColors else LightColors MaterialTheme( - colorScheme = colorScheme, + colorScheme = colors, typography = Typography, content = content ) -} \ No newline at end of file +} 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 new file mode 100644 index 0000000..1808733 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/ui/theme/ThemeToggle.kt @@ -0,0 +1,35 @@ +package com.example.wifip2photspot.ui.theme + +// ThemeToggle.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 +import com.example.wifip2photspot.SwitchPreference + +@Composable +fun ThemeToggle( + isDarkTheme: Boolean, + onToggle: (Boolean) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { +// Text("Dark Theme", style = MaterialTheme.typography.bodyLarge) + SwitchPreference( + label = "Dark Theme", + checked = isDarkTheme, + onCheckedChange = onToggle + + ) + } +} + + diff --git a/app/src/main/java/com/example/wifip2photspot/viewModel/AppModule.kt b/app/src/main/java/com/example/wifip2photspot/viewModel/AppModule.kt new file mode 100644 index 0000000..4ef89f5 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/viewModel/AppModule.kt @@ -0,0 +1,36 @@ +package com.example.wifip2photspot.viewModel +// +//// AppModule.kt +// +// +//import android.app.NotificationManager +//import android.content.Context +//import android.net.wifi.p2p.WifiP2pManager +//import dagger.Module +//import dagger.Provides +//import dagger.hilt.InstallIn +//import dagger.hilt.components.SingletonComponent +//import javax.inject.Singleton +// +//@androidx.test.espresso.core.internal.deps.dagger.Module +//@InstallIn(SingletonComponent::class) +//object AppModule { +// +// @Provides +// @Singleton +// fun provideWifiP2pManager(application: Application): WifiP2pManager { +// return application.getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager +// } +// +// @Provides +// @Singleton +// fun provideNotificationManager(application: Application): NotificationManager { +// return application.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager +// } +// +// @Provides +// @Singleton +// fun provideVpnRepository(application: Application): VpnRepository { +// return VpnRepository(application) +// } +//} 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..2653a6f --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/viewModel/HotspotViewModel.kt @@ -0,0 +1,1304 @@ +// HotspotViewModel.kt +package com.example.wifip2photspot.viewModel + +import android.app.Application +import android.app.Notification +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.net.TrafficStats +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.BatteryManager +import android.os.Build +import android.os.Looper +import android.service.controls.ControlsProviderService.TAG +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.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.contentcapture.ContentCaptureManager.Companion.isEnabled +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.example.wifip2photspot.DeviceInfo +import com.example.wifip2photspot.R +import com.example.wifip2photspot.StartHotspotWorker +import com.example.wifip2photspot.StopHotspotWorker +import com.example.wifip2photspot.VPN.MyVpnService +import com.example.wifip2photspot.VPN.VpnRepository +import com.example.wifip2photspot.socksProxy.SSHServerManager + +import com.github.mikephil.charting.data.Entry +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.delay +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.io.IOException +import java.util.concurrent.TimeUnit +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +@RequiresApi(Build.VERSION_CODES.Q) +class HotspotViewModel( + application: Context, + private val dataStore: DataStore, + private val vpnRepository: VpnRepository +) : AndroidViewModel(application as Application) { + // ----- DataStore Keys ----- + companion object { + val SSID_KEY = stringPreferencesKey("ssid") + val PASSWORD_KEY = stringPreferencesKey("password") + 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()) + val BIND_TO_MOBILE_DATA_KEY = booleanPreferencesKey("bind_to_mobile_data") // New Key + + + + } + + // Preferences Keys + private object PreferencesKeys { + val WIFI_LOCK_ENABLED = booleanPreferencesKey("wifi_lock_enabled") + private val DATA_USAGE_KEY = stringPreferencesKey("device_usage_key") + private val DARK_THEME_KEY = stringPreferencesKey("dark_theme_key") + + + // Other preference keys... + } + + // Enum for themes + enum class AppTheme { + DEFAULT, DARK, AMOLED, CUSTOM + } + + // ----- 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 + + + // Dark Theme State + private val _isDarkTheme = MutableStateFlow(false) + val isDarkTheme: StateFlow = _isDarkTheme.asStateFlow() + + + // ----- 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() + + //visible password + private var passwordVisible by mutableStateOf(false) + // SSH Server Configuration + private var sshServerManager: SSHServerManager? = null + + // ----- StateFlows and Variables ----- + private val _bindToMobileData = MutableStateFlow(false) + val bindToMobileData: StateFlow = _bindToMobileData.asStateFlow() + + + + // ----- 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() + } + + // 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() + + private val sshLocalPort = 2222 // SSH Server Port + private val proxyPort = 8181 // SOCKS Proxy Port + + + // Load aliases in init block +// +// 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() + + // Notification Settings State + private val _notificationEnabled = MutableStateFlow(true) + val notificationEnabled: StateFlow = _notificationEnabled.asStateFlow() + + private val notificationManager = + application.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + private val _notificationSoundEnabled = MutableStateFlow(true) + val notificationSoundEnabled: StateFlow = _notificationSoundEnabled.asStateFlow() + + private val _notificationVibrationEnabled = MutableStateFlow(true) + val notificationVibrationEnabled: StateFlow = + _notificationVibrationEnabled.asStateFlow() + + //battery level + private val _batteryLevel = MutableStateFlow(100) + val batteryLevel: StateFlow = _batteryLevel.asStateFlow() + + private val _networkQuality = MutableStateFlow("Good") + val networkQuality: StateFlow = _networkQuality.asStateFlow() + + + private val _isIdle = MutableStateFlow(false) + val isIdle: StateFlow = _isIdle.asStateFlow() + +// private val _remainingIdleTime = MutableStateFlow(0L) +// val remainingIdleTime: StateFlow = _remainingIdleTime.asStateFlow() + + + + private val idleThresholdBytesPerSecond = 100L // Define your threshold + private val idleCheckIntervalMillis = 30000L // Check every 30 seconds + private val idleTimeoutMillis = 600000L // 10 minutes of inactivity + private var lastTotalBytes: Long = 0L + + // private var lastTotalBytes: Long = 0L + private var idleStartTime: Long = 0L + private var isCountingDown: Boolean = false + + private val _appTheme = MutableStateFlow(AppTheme.DEFAULT) + val appTheme: StateFlow = _appTheme.asStateFlow() + + // 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 socksProxyManager = SocksProxyManager(proxyPort = proxyPort) + + +// // Proxy server properties +// private val _isProxyRunning = MutableStateFlow(false) +// val isProxyRunning: StateFlow = _isProxyRunning.asStateFlow() +// +// private val _proxyPort = MutableStateFlow(8080) +// val proxyPort: StateFlow = _proxyPort.asStateFlow() + + + // // Update Proxy Port +// fun updateProxyPort(port: Int) { +// _proxyPort.value = port +// } + // SSH Server Configuration + + + // ----- Function to Update Bind to Mobile Data Preference ----- + fun updateBindToMobileData(enabled: Boolean) { + _bindToMobileData.value = enabled + viewModelScope.launch { + dataStore.edit { preferences -> + preferences[BIND_TO_MOBILE_DATA_KEY] = enabled + } + if (_isHotspotEnabled.value) { + // Restart SSH server with updated binding preference + sshServerManager?.stopSSHServer() + sshServerManager?.startSSHServer( + localPort = sshLocalPort +// bindToMobileData = enabled + ) + updateLog("SSH Server binding to mobile data: $enabled") + } + } + } + + + + /////////////////// + // VPN Control + private val _isVpnActive = MutableStateFlow(false) + val isVpnActive: StateFlow = _isVpnActive.asStateFlow() + + // ----- SSH Server Management ----- + // Start SSH Server + private fun startSSHServer() { + viewModelScope.launch { + sshServerManager?.startSSHServer( + localPort = sshLocalPort, +// bindToMobileData = _bindToMobileData.value + ) + updateLog("SSH Server started on port $sshLocalPort") + } + } + + // Stop SSH Server + private fun stopSSHServer() { + viewModelScope.launch { + sshServerManager?.stopSSHServer() + updateLog("SSH Server stopped.") + } + } + + + init { + // ----- Load SSID and Password from DataStore ----- + // ----- Load SSID and Password from DataStore ----- + viewModelScope.launch { + dataStore.data + .catch { exception -> + if (exception is IOException) { + emit(emptyPreferences()) + } else { + throw exception + } + } + .collect { preferences -> + val newSSID = preferences[SSID_KEY] ?: "TetherGuard" + val newPassword = preferences[PASSWORD_KEY] ?: "00000000" + _ssid.value = newSSID + _password.value = newPassword + + // Initialize SSHServerManager with updated credentials + sshServerManager?.stopSSHServer() + sshServerManager = SSHServerManager( + context = getApplication().applicationContext, + sshUsername = newSSID, + sshPassword = newPassword, + proxyPort = proxyPort + ) + + // If hotspot is already enabled, restart SSH server with new credentials + if (_isHotspotEnabled.value) { + // Stop and restart SSH server to apply new credentials + sshServerManager?.stopSSHServer() + sshServerManager?.startSSHServer( + localPort = sshLocalPort +// bindToMobileData = _bindToMobileData.value + ) + updateLog("SSH Server updated with new SSID and Password.") + } + } + } + + // 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 + _bindToMobileData.value = preferences[BIND_TO_MOBILE_DATA_KEY] ?: false + // Load other preferences... + } + } + // ----- Start Monitoring Network Speeds ----- + 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) + } + } + + monitorNetworkSpeeds() +//// Start additional monitoring services + startBatteryMonitoring() + startNetworkMonitoring() +// viewModelScope.launch { +// dataStore.data.collect { preferences -> +// _dataUsageThreshold.value = preferences[DATA_USAGE_THRESHOLD_KEY] ?: 0L +// } +// } + } + + private fun startVpnService() { + val vpnIntent = Intent(getApplication(), MyVpnService::class.java) + getApplication().startService(vpnIntent) + updateLog("VPN Service Started via HotspotViewModel.") + } + + private fun stopVpnService() { + val vpnIntent = Intent(getApplication(), MyVpnService::class.java) + getApplication().stopService(vpnIntent) + updateLog("VPN Service Stopped via HotspotViewModel.") + } +// // ----- Methods to Handle BroadcastReceiver Calls ----- +// fun startTethering(ssid: String, password: String, selectedBand: Int) { +// viewModelScope.launch { +// _isProcessing.value = true +// // Start Wi-Fi Direct group with specified SSID, password, and band +// // Implement group creation logic here using WifiP2pManager APIs +// // Once the group is successfully formed, the BroadcastReceiver will handle the rest +// _isHotspotEnabled.value = true +// updateLog("Tethering started with SSID: $ssid") +// // Start VPN via repository +// vpnRepository.startVpn() +// _isProcessing.value = false +// } +// } +// +// fun stopTethering() { +// viewModelScope.launch { +// _isProcessing.value = true +// // Stop Wi-Fi Direct group +// // Implement group disbanding logic here using WifiP2pManager APIs +// _isHotspotEnabled.value = false +// updateLog("Tethering stopped.") +// // Stop VPN via repository +// vpnRepository.stopVpn() +// _isProcessing.value = false +// } +// } + + /////////////////////////////////////////////////////// + @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) + } + } + } + } + + + + // ----- 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.") + // Start SSH Server + startSSHServer() + // Start VPN via repository + vpnRepository.startVpn() +// startVpn() + } else { + _isHotspotEnabled.value = false + updateLog("Device is Client. SSH Server and VPN are not required.") + // Optionally, handle client-specific logic + } + } + } + +// // Function to start VPN +// private fun startVpn() { +// val vpnIntent = Intent(getApplication(), MyVpnService::class.java) +// getApplication().startService(vpnIntent) +// _isVpnActive.value = true +// updateLog("VPN Service Started") +// viewModelScope.launch { +// _eventFlow.emit(UiEvent.ShowToast("VPN started successfully.")) +// } +// } +// +// // Function to stop VPN +// private fun stopVpn() { +// val vpnIntent = Intent(getApplication(), MyVpnService::class.java) +// getApplication().stopService(vpnIntent) +// _isVpnActive.value = false +// updateLog("VPN Service Stopped") +// viewModelScope.launch { +// _eventFlow.emit(UiEvent.ShowToast("VPN stopped successfully.")) +// } +// } + ////////*(((((((((((***********))))))))))))///////////////////// //////////////////////////////working fine ////////////////////////////////// + // ----- Helper Methods ----- + fun updateLog(message: String) { + viewModelScope.launch { + _logEntries.value += message + // Alternatively, implement a logging mechanism or update a StateFlow for logs + } + } + + // ----- Function to Handle Device List Changes ----- + fun onDevicesChanged(deviceList: Collection) { + val previousDevices = _connectedDevices.value + updateLog("Connected Devices: ${deviceList.size}") + _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 + ) + } + } + fun onDisconnected() { + viewModelScope.launch { + _isHotspotEnabled.value = false + updateLog("Group disbanded. Stopping SSH Server and VPN.") + // Stop SSH Server + stopSSHServer() + // Stop VPN via repository + vpnRepository.stopVpn() +// stopVpn() + } + } + // ----- Lifecycle Management ----- + override fun onCleared() { + super.onCleared() + releaseWifiLock() + stopSSHServer() + onButtonStopTapped() + updateLog("SSH Server and Proxy Server cleaned up.") + // Cancel any ongoing coroutines if necessary + } + + + // ----- 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 ----- + 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 Tethering ----- + fun startTethering() { + viewModelScope.launch { + _isProcessing.value = true + try { + // Start Wi-Fi Direct group with SSID and Password + onButtonStartTapped(ssid.value, password.value, selectedBand.value) + } catch (e: Exception) { + Log.e(TAG, "Error starting tethering: ${e.message}") + _isHotspotEnabled.value = false + _isProcessing.value = false + _eventFlow.emit(UiEvent.ShowToast("Error starting tethering: ${e.message}")) + } + } + } + // ----- Function to Handle Start Hotspot Button Tapped ----- + private 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 + _isProcessing.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: $ssidTrimmed") + 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.")) + + // Start VPN + vpnRepository.startVpn() +// startVpn() + + // Start SSH Server + startSSHServer() + + // Acquire Wi-Fi Lock if enabled + acquireWifiLock() + + // Show Hotspot Status Notification + showHotspotStatusNotification() + + // Start Idle Monitoring + startIdleMonitoring() + + // Start Data Usage Tracking + startDataUsageTracking() + + // Start Battery Monitoring + startBatteryMonitoring() + + // Start Network Monitoring + startNetworkMonitoring() + } + } + + 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 { + suspendCancellableCoroutine { cont -> + 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.")) + + Log.d(TAG, "Wi-Fi Direct group removed successfully.") + cont.resume(Unit) + + // Stop VPN + vpnRepository.stopVpn() +// stopVpn() + + // Stop SSH Server + stopSSHServer() + + // Release Wi-Fi Lock if acquired + releaseWifiLock() + + // Remove Hotspot Status Notification + removeHotspotStatusNotification() + + // Reset Idle Monitoring + _remainingIdleTime.value = 0L + } + } + 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}")) + } + } + } + } + + //*****************************************************Fine**************************************** + + fun startNetworkMonitoring() { + val connectivityManager = + getApplication().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + val linkDownstreamBandwidthKbps = networkCapabilities.linkDownstreamBandwidthKbps + val linkUpstreamBandwidthKbps = networkCapabilities.linkUpstreamBandwidthKbps + + val quality = when { + linkDownstreamBandwidthKbps < 150 -> "Poor" + linkDownstreamBandwidthKbps < 550 -> "Moderate" + linkDownstreamBandwidthKbps < 2000 -> "Good" + else -> "Excellent" + } + _networkQuality.value = quality + } + } + + val networkRequest = NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build() + + connectivityManager.registerNetworkCallback(networkRequest, networkCallback) + } + + fun startBatteryMonitoring() { + val batteryManager = + getApplication().getSystemService(Context.BATTERY_SERVICE) as BatteryManager + val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + val batteryStatus = getApplication().registerReceiver(null, intentFilter) + batteryStatus?.let { + val level = it.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + val scale = it.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + val batteryPct = level * 100 / scale.toFloat() + _batteryLevel.value = batteryPct.toInt() + } + + viewModelScope.launch { + while (isHotspotEnabled.value) { + delay(60000) // Check every minute + val level = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) + _batteryLevel.value = level + + if (level <= 15) { + withContext(Dispatchers.Main) { + _eventFlow.emit(UiEvent.ShowToast("Battery low ($level%). Consider turning off the hotspot to save power.")) + } + } + } + } + } + + // 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 + } + } + } + + fun acquireWifiLock() { + if (_wifiLockEnabled.value) { + val wifiManager = + getApplication().getSystemService(Context.WIFI_SERVICE) as WifiManager + wifiLock = wifiManager.createWifiLock( + WifiManager.WIFI_MODE_FULL_HIGH_PERF, + "WiFiP2PHotspotLock" + ) + wifiLock?.acquire() + } + } + + fun releaseWifiLock() { + wifiLock?.let { + if (it.isHeld) { + it.release() + } + } + } + + // ---------- Idle Monitoring Logic ---------- + fun startIdleMonitoring() { + viewModelScope.launch { + var idleStartTime = System.currentTimeMillis() + lastTotalBytes = 0L // Initialize lastTotalBytes + while (isHotspotEnabled.value && autoShutdownEnabled.value) { + delay(1000L) // Update every second + val connectedDevices = _connectedDevices.value + val (rxBytes, txBytes) = getSessionDataUsage() + val currentTotalBytes = rxBytes + txBytes + + val dataUsageInInterval: Long + if (lastTotalBytes == 0L) { + dataUsageInInterval = 0L // No previous data to compare + } else { + dataUsageInInterval = currentTotalBytes - lastTotalBytes + } + lastTotalBytes = currentTotalBytes + + val dataUsagePerSecond = dataUsageInInterval / (1000L) // Since we delay for 1000ms + + _isIdle.value = + connectedDevices.isEmpty() || dataUsagePerSecond < idleThresholdBytesPerSecond + + if (_isIdle.value && _autoShutdownEnabled.value) { + val elapsedIdleTime = System.currentTimeMillis() - idleStartTime + val totalIdleTime = _idleTimeoutMinutes.value * 60 * 1000L + _remainingIdleTime.value = totalIdleTime - elapsedIdleTime + + if (_remainingIdleTime.value <= 0L) { + // Idle time exceeded, turn off hotspot + withContext(Dispatchers.Main) { + onButtonStopTapped() + _eventFlow.emit(UiEvent.ShowToast("Hotspot turned off due to inactivity")) + } + break + } + } else { + idleStartTime = System.currentTimeMillis() + _remainingIdleTime.value = _idleTimeoutMinutes.value * 60 * 1000L + } + } + // Reset remaining idle time when monitoring stops + _remainingIdleTime.value = 0L + } + } + + // Function to get session data usage (implementation depends on previous steps) + fun getSessionDataUsage(): Pair { + val rxBytes = TrafficStats.getTotalRxBytes() + val txBytes = TrafficStats.getTotalTxBytes() + return Pair(rxBytes, txBytes) + } + + 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")) + } + } + } + + // Function to send notifications + fun sendDeviceConnectionNotification(deviceName: String) { + viewModelScope.launch { + val notification: Notification = + NotificationCompat.Builder(getApplication(), "device_connection_channel") + .setSmallIcon(R.drawable.ic_notification) // Ensure you have a valid icon + .setContentTitle("Device Connected") + .setContentText("$deviceName has connected to the hotspot.") + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .build() + + notificationManager.notify(1185509172, notification) + } + } + + + private 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 startDataUsageTracking() { + sessionStartRxBytes = TrafficStats.getTotalRxBytes() + sessionStartTxBytes = TrafficStats.getTotalTxBytes() + } + + // 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) { + // 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 + } + + fun updateTheme(isDark: Boolean) { + _isDarkTheme.value = isDark + // Save to DataStore if needed + } + + fun attemptReconnection() { + viewModelScope.launch { + var attempts = 0 + val maxAttempts = 5 + val delayBetweenAttempts = 5000L // 5 seconds + + while (attempts < maxAttempts && !isConnected()) { + attempts++ + connectToGroup() + delay(delayBetweenAttempts) + } + if (!isConnected()) { + _eventFlow.emit(UiEvent.ShowToast("Failed to reconnect after $attempts attempts.")) + } + } + } + private fun isConnected(): Boolean { + // Implement logic to check if the hotspot is connected + return isHotspotEnabled.value && _connectedDevices.value.isNotEmpty() + } + private fun connectToGroup() { + initializeGroup() + } +} diff --git a/app/src/main/java/com/example/wifip2photspot/viewModel/HotspotViewModelFactory.kt b/app/src/main/java/com/example/wifip2photspot/viewModel/HotspotViewModelFactory.kt new file mode 100644 index 0000000..f900c87 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/viewModel/HotspotViewModelFactory.kt @@ -0,0 +1,41 @@ +// HotspotViewModelFactory.kt +package com.example.wifip2photspot.viewModel + +import android.app.Application +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.datastore.preferences.core.Preferences +import com.example.wifip2photspot.VPN.VpnRepository + +class HotspotViewModelFactory( + private val application: Application, + private val dataStore: DataStore, + private val vpnRepository: VpnRepository +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(HotspotViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return HotspotViewModel(application, dataStore, vpnRepository) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} + + + +// +// +//class HotspotViewModelFactory( +// private val application: Application, +// private val dataStore: DataStore +//) : ViewModelProvider.Factory { +// override fun create(modelClass: Class): T { +// if (modelClass.isAssignableFrom(HotspotViewModel::class.java)) { +// @Suppress("UNCHECKED_CAST") +// return HotspotViewModel(application, dataStore) as T +// } +// throw IllegalArgumentException("Unknown ViewModel class") +// } +//} diff --git a/app/src/main/java/com/example/wifip2photspot/viewModel/VpnViewModel.kt b/app/src/main/java/com/example/wifip2photspot/viewModel/VpnViewModel.kt new file mode 100644 index 0000000..3b3ead3 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/viewModel/VpnViewModel.kt @@ -0,0 +1,105 @@ +package com.example.wifip2photspot.viewModel + +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import android.app.Application +import android.content.Context +import android.content.Intent +import android.net.VpnService +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.example.wifip2photspot.VPN.VpnRepository +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch + + +class VpnViewModel( + application: Application, + private val dataStore: DataStore, + private val vpnRepository: VpnRepository +) : AndroidViewModel(application) { + + // ----- DataStore Keys ----- + companion object { + val VPN_ENABLED_KEY = booleanPreferencesKey("vpn_enabled") + } + + // ----- StateFlows for UI State ----- + private val _isVpnActive = MutableStateFlow(false) + val isVpnActive: StateFlow = _isVpnActive.asStateFlow() + + private val _vpnStatusMessage = MutableStateFlow("") + val vpnStatusMessage: StateFlow = _vpnStatusMessage.asStateFlow() + + // ----- UI Events ----- + private val _eventFlow = MutableSharedFlow() + val eventFlow: SharedFlow = _eventFlow.asSharedFlow() + + // ----- Sealed Class for UI Events ----- + sealed class UiEvent { + data class ShowToast(val message: String) : UiEvent() + object StartVpnService : UiEvent() + object StopVpnService : UiEvent() + } + + + +// init { +// // Load VPN Enabled Preference from DataStore +// viewModelScope.launch { +// dataStore.data.collect { preferences -> +// val vpnEnabled = preferences[VPN_ENABLED_KEY] ?: false +// if (vpnEnabled && !_isVpnActive.value) { +// startVpn() +// } else if (!vpnEnabled && _isVpnActive.value) { +// stopVpn() +// } +// } +// } +// } + + // // ----- Functions to Manage VPN ----- +// fun toggleVpn(enabled: Boolean) { +// viewModelScope.launch { +// dataStore.edit { preferences -> +// preferences[VPN_ENABLED_KEY] = enabled +// } +// if (enabled) { +// startVpn() +// } else { +// stopVpn() +// } +// } +// } + // ----- Functions to Manage VPN ----- + fun toggleVpn(enabled: Boolean) { + viewModelScope.launch { + if (enabled) { + startVpn() + } else { + stopVpn() + } + } + } + + private fun startVpn() { + viewModelScope.launch { + _eventFlow.emit(UiEvent.StartVpnService) + _isVpnActive.value = true + _vpnStatusMessage.value = "VPN Started" + _eventFlow.emit(UiEvent.ShowToast("VPN started successfully.")) + } + } + + private fun stopVpn() { + viewModelScope.launch { + _eventFlow.emit(UiEvent.StopVpnService) + _isVpnActive.value = false + _vpnStatusMessage.value = "VPN Stopped" + _eventFlow.emit(UiEvent.ShowToast("VPN stopped successfully.")) + } + } +} + diff --git a/app/src/main/java/com/example/wifip2photspot/viewModel/VpnViewModelFactory.kt b/app/src/main/java/com/example/wifip2photspot/viewModel/VpnViewModelFactory.kt new file mode 100644 index 0000000..744767f --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/viewModel/VpnViewModelFactory.kt @@ -0,0 +1,25 @@ +package com.example.wifip2photspot.viewModel + +// VpnViewModelFactory.kt + +import android.app.Application +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import com.example.wifip2photspot.VPN.VpnRepository + +class VpnViewModelFactory( + private val application: Application, + private val dataStore: DataStore, + private val vpnRepository: VpnRepository +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(VpnViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return VpnViewModel(application, dataStore,vpnRepository) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file 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..9bdc973 --- /dev/null +++ b/app/src/main/java/com/example/wifip2photspot/viewModel/WiFiDirectBroadcastReceiver.kt @@ -0,0 +1,80 @@ +// 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 + +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}") + // Inform ViewModel that the device is now the Group Owner + CoroutineScope(Dispatchers.Main).launch { + viewModel.onGroupOwnerChanged(true) + } + } else { + viewModel.updateLog("Connected as a client.") + // Inform ViewModel that the device is a client + CoroutineScope(Dispatchers.Main).launch { + viewModel.onGroupOwnerChanged(false) + } + } + + // Request group info to get connected devices + manager.requestGroupInfo(channel) { group -> + if (group != null) { + val deviceList = group.clientList + viewModel.onDevicesChanged(deviceList) + viewModel.updateLog("Connected Devices: ${deviceList.size}") + } + } + } + } else { + // Disconnected from a P2P network + viewModel.onDevicesChanged(emptyList()) + viewModel.updateLog("Disconnected from group.") + // Inform ViewModel to handle disconnection + CoroutineScope(Dispatchers.Main).launch { + viewModel.onDisconnected() + } + } + } + + // Handle other actions if necessary + WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> { + // Optionally handle changes to this device's Wi-Fi state + // For example, update device details in the UI + // Not essential for VPN functionality + } + } + } +} + diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 0000000..5979e31 --- /dev/null +++ b/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_proxy.xml b/app/src/main/res/drawable/ic_proxy.xml new file mode 100644 index 0000000..a0b2713 --- /dev/null +++ b/app/src/main/res/drawable/ic_proxy.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_proxy_notification.xml b/app/src/main/res/drawable/ic_proxy_notification.xml new file mode 100644 index 0000000..a8b409b --- /dev/null +++ b/app/src/main/res/drawable/ic_proxy_notification.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/raw/server_certificate.crt b/app/src/main/res/raw/server_certificate.crt new file mode 100644 index 0000000..5eb1db4 --- /dev/null +++ b/app/src/main/res/raw/server_certificate.crt @@ -0,0 +1,14 @@ +private fun createSSLSocket(serverAddress: String, serverPort: Int): Socket? { + return try { + val sslSocketFactory = createPinnedSSLSocketFactory() + ?: throw IOException("Failed to create pinned SSLSocketFactory") + val sslSocket = sslSocketFactory.createSocket() as Socket + 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 + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7e08955..c49500f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,19 @@ + - wifip2photspot - \ No newline at end of file + WiFi P2P Hotspot + Speed + Download + Devices + kbps + Error fetching data. Please try again. + A device has connected. + A device has disconnected. + Device connected. Total devices: %1$d + Device disconnected. Total devices: %1$d + Hotspot is active + Hotspot is inactive + Upload Speed + Download Speed + Number of connected devices + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8cca656..b79273f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,23 @@ [versions] agp = "8.6.0" +charts = "1.0.6" kotlin = "1.9.0" coreKtx = "1.13.1" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" +kotlinxSerializationJson = "1.5.1" lifecycleRuntimeKtx = "2.8.6" activityCompose = "1.9.3" composeBom = "2024.04.01" composeMaterial = "1.4.0" +mpandroidchart = "v3.1.0" +timber = "5.0.1" +datastorePreferencesCoreJvm = "1.1.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +charts = { module = "com.himanshoe:charts", version.ref = "charts" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } @@ -26,6 +32,10 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } 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" } +mpandroidchart = { module = "com.github.PhilJay:MPAndroidChart", version.ref = "mpandroidchart" } +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" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 1455624..0180f42 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,9 +16,12 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + + // Add JitPack repository + maven("https://jitpack.io") } } rootProject.name = "wifip2photspot" include(":app") - \ No newline at end of file +