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