mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf8d9c6e4b | |||
| 1892b9b4a2 | |||
| 0f246bf066 | |||
| 0b87d4f23e |
@@ -219,6 +219,10 @@ dependencies {
|
|||||||
implementation(libs.ktor.client.content.negotiation)
|
implementation(libs.ktor.client.content.negotiation)
|
||||||
implementation(libs.ktor.serialization.kotlinx.json)
|
implementation(libs.ktor.serialization.kotlinx.json)
|
||||||
implementation(libs.slf4j.android)
|
implementation(libs.slf4j.android)
|
||||||
|
|
||||||
|
// shizuku
|
||||||
|
implementation(libs.shizuku.api)
|
||||||
|
implementation(libs.shizuku.provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Copy>("copyLicenseeJsonToAssets") {
|
tasks.register<Copy>("copyLicenseeJsonToAssets") {
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private var lastLocationPermissionState: Boolean? = null
|
private var lastLocationPermissionState: Boolean? = null
|
||||||
|
|
||||||
|
val REQUEST_CODE = 123
|
||||||
|
|
||||||
@SuppressLint("BatteryLife")
|
@SuppressLint("BatteryLife")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge(
|
enableEdgeToEdge(
|
||||||
|
|||||||
+2
-14
@@ -24,22 +24,10 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.NonCancellable
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
|||||||
+2
-15
@@ -29,21 +29,8 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Provider
|
import javax.inject.Provider
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.FlowPreview
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.debounce
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ import dagger.hilt.components.SingletonComponent
|
|||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import org.amnezia.awg.backend.Backend
|
import org.amnezia.awg.backend.Backend
|
||||||
import org.amnezia.awg.backend.GoBackend
|
import org.amnezia.awg.backend.GoBackend
|
||||||
import org.amnezia.awg.backend.RootTunnelActionHandler
|
import org.amnezia.awg.backend.RootTunnelActionHandler
|
||||||
@@ -112,11 +114,22 @@ class TunnelModule {
|
|||||||
fun provideNetworkMonitor(
|
fun provideNetworkMonitor(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
settingsRepository: AppSettingRepository,
|
settingsRepository: AppSettingRepository,
|
||||||
|
@ApplicationScope applicationScope: CoroutineScope,
|
||||||
|
@AppShell appShell: RootShell,
|
||||||
): NetworkMonitor {
|
): NetworkMonitor {
|
||||||
val method = runBlocking { settingsRepository.get().wifiDetectionMethod }
|
|
||||||
return AndroidNetworkMonitor(
|
return AndroidNetworkMonitor(
|
||||||
context,
|
context,
|
||||||
AndroidNetworkMonitor.WifiDetectionMethod.fromValue(method.value),
|
object : AndroidNetworkMonitor.ConfigurationListener {
|
||||||
|
override val detectionMethod: Flow<AndroidNetworkMonitor.WifiDetectionMethod>
|
||||||
|
get() =
|
||||||
|
settingsRepository.flow
|
||||||
|
.distinctUntilChangedBy { it.wifiDetectionMethod }
|
||||||
|
.map { it.wifiDetectionMethod }
|
||||||
|
|
||||||
|
override val rootShell: RootShell
|
||||||
|
get() = appShell
|
||||||
|
},
|
||||||
|
applicationScope,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
@@ -204,6 +204,8 @@ fun WifiTunnelingItems(
|
|||||||
if (
|
if (
|
||||||
uiState.appSettings.wifiDetectionMethod ==
|
uiState.appSettings.wifiDetectionMethod ==
|
||||||
AndroidNetworkMonitor.WifiDetectionMethod.ROOT ||
|
AndroidNetworkMonitor.WifiDetectionMethod.ROOT ||
|
||||||
|
uiState.appSettings.wifiDetectionMethod ==
|
||||||
|
AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU ||
|
||||||
isWifiNameReadable()
|
isWifiNameReadable()
|
||||||
) {
|
) {
|
||||||
viewModel.handleEvent(AppEvent.SaveTrustedSSID(ssid))
|
viewModel.handleEvent(AppEvent.SaveTrustedSSID(ssid))
|
||||||
|
|||||||
-2
@@ -28,8 +28,6 @@ fun WifiDetectionMethodScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
|||||||
enumValues<AndroidNetworkMonitor.WifiDetectionMethod>().forEach {
|
enumValues<AndroidNetworkMonitor.WifiDetectionMethod>().forEach {
|
||||||
val title = it.asString(context)
|
val title = it.asString(context)
|
||||||
val description = it.asDescriptionString(context)
|
val description = it.asDescriptionString(context)
|
||||||
// TODO skip shizuku for now
|
|
||||||
if (it == AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU) return@forEach
|
|
||||||
IconSurfaceButton(
|
IconSurfaceButton(
|
||||||
title = title,
|
title = title,
|
||||||
onClick = { viewModel.handleEvent(AppEvent.SetDetectionMethod(it)) },
|
onClick = { viewModel.handleEvent(AppEvent.SetDetectionMethod(it)) },
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
@@ -46,6 +47,7 @@ import kotlinx.coroutines.sync.Mutex
|
|||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.amnezia.awg.config.BadConfigException
|
import org.amnezia.awg.config.BadConfigException
|
||||||
import org.amnezia.awg.config.Config
|
import org.amnezia.awg.config.Config
|
||||||
|
import rikka.shizuku.Shizuku
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||||
|
|
||||||
@@ -223,16 +225,31 @@ constructor(
|
|||||||
) {
|
) {
|
||||||
if (detectionMethod == appSettings.wifiDetectionMethod) return
|
if (detectionMethod == appSettings.wifiDetectionMethod) return
|
||||||
when (detectionMethod) {
|
when (detectionMethod) {
|
||||||
AndroidNetworkMonitor.WifiDetectionMethod.ROOT -> {
|
AndroidNetworkMonitor.WifiDetectionMethod.ROOT -> if (!requestRoot()) return
|
||||||
val allowed = requestRoot()
|
AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU -> {
|
||||||
if (!allowed) return
|
Shizuku.addRequestPermissionResultListener(
|
||||||
|
Shizuku.OnRequestPermissionResultListener { requestCode: Int, grantResult: Int
|
||||||
|
->
|
||||||
|
if (grantResult != PERMISSION_GRANTED)
|
||||||
|
return@OnRequestPermissionResultListener
|
||||||
|
viewModelScope.launch {
|
||||||
|
saveSettings(appSettings.copy(wifiDetectionMethod = detectionMethod))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
if (Shizuku.checkSelfPermission() != PERMISSION_GRANTED)
|
||||||
|
return Shizuku.requestPermission(123)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
return handleShowMessage(
|
||||||
|
StringValue.StringResource(R.string.shizuku_not_detected)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// TODO check if shizuku available
|
|
||||||
AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU -> Unit
|
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
saveSettings(appSettings.copy(wifiDetectionMethod = detectionMethod))
|
saveSettings(appSettings.copy(wifiDetectionMethod = detectionMethod))
|
||||||
handleShowMessage(StringValue.StringResource(R.string.app_restart_required))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleToggleSelectAllTunnels(tunnels: List<TunnelConf>) =
|
private fun handleToggleSelectAllTunnels(tunnels: List<TunnelConf>) =
|
||||||
|
|||||||
@@ -280,4 +280,5 @@
|
|||||||
Android version
|
Android version
|
||||||
</string>
|
</string>
|
||||||
<string name="release_notes">Release notes</string>
|
<string name="release_notes">Release notes</string>
|
||||||
|
<string name="shizuku_not_detected">Shizuku not detected</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -13,5 +13,5 @@ object Constants {
|
|||||||
const val PRERELEASE = "prerelease"
|
const val PRERELEASE = "prerelease"
|
||||||
|
|
||||||
val allowedLicenses = listOf("MIT", "Apache-2.0", "BSD-3-Clause")
|
val allowedLicenses = listOf("MIT", "Apache-2.0", "BSD-3-Clause")
|
||||||
val allowedLicenseUrls = listOf("https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING")
|
val allowedLicenseUrls = listOf("https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING", "https://github.com/RikkaApps/Shizuku-API/blob/master/LICENSE")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ accompanist = "0.37.3"
|
|||||||
activityCompose = "1.10.1"
|
activityCompose = "1.10.1"
|
||||||
amneziawgAndroid = "1.4.0"
|
amneziawgAndroid = "1.4.0"
|
||||||
androidx-junit = "1.2.1"
|
androidx-junit = "1.2.1"
|
||||||
|
shizuku = "13.1.5"
|
||||||
appcompat = "1.7.1"
|
appcompat = "1.7.1"
|
||||||
biometricKtx = "1.2.0-alpha05"
|
biometricKtx = "1.2.0-alpha05"
|
||||||
coreKtx = "1.16.0"
|
coreKtx = "1.16.0"
|
||||||
@@ -100,6 +101,8 @@ material-icons-extended = { module = "androidx.compose.material:material-icons-e
|
|||||||
|
|
||||||
# util
|
# util
|
||||||
pin-lock-compose = { module = "com.zaneschepke:pin_lock_compose", version.ref = "pinLockCompose" }
|
pin-lock-compose = { module = "com.zaneschepke:pin_lock_compose", version.ref = "pinLockCompose" }
|
||||||
|
shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" }
|
||||||
|
shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" }
|
||||||
qrose = { module = "io.github.alexzhirkevich:qrose", version.ref = "qrose" }
|
qrose = { module = "io.github.alexzhirkevich:qrose", version.ref = "qrose" }
|
||||||
semver4j = { module = "com.vdurmont:semver4j", version.ref = "semver4j" }
|
semver4j = { module = "com.vdurmont:semver4j", version.ref = "semver4j" }
|
||||||
slf4j-android = { module = "org.slf4j:slf4j-android", version.ref = "slf4jAndroid" }
|
slf4j-android = { module = "org.slf4j:slf4j-android", version.ref = "slf4jAndroid" }
|
||||||
|
|||||||
@@ -43,5 +43,9 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.tunnel)
|
implementation(libs.tunnel)
|
||||||
|
|
||||||
|
// shizuku
|
||||||
|
implementation(libs.shizuku.api)
|
||||||
|
implementation(libs.shizuku.provider)
|
||||||
|
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,15 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"
|
||||||
tools:targetApi="29" />
|
tools:targetApi="29" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<provider
|
||||||
|
android:name="rikka.shizuku.ShizukuProvider"
|
||||||
|
android:authorities="${applicationId}.shizuku"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:multiprocess="false"
|
||||||
|
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||||
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
||||||
|
|||||||
+70
-40
@@ -12,25 +12,29 @@ import android.net.NetworkRequest
|
|||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.wireguard.android.util.RootShell
|
import com.wireguard.android.util.RootShell
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import com.zaneschepke.networkmonitor.shizuku.ShizukuShell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetectionMethod) :
|
class AndroidNetworkMonitor(
|
||||||
NetworkMonitor {
|
private val appContext: Context,
|
||||||
|
private val configurationListener: ConfigurationListener,
|
||||||
|
private val applicationScope: CoroutineScope,
|
||||||
|
) : NetworkMonitor {
|
||||||
|
|
||||||
|
interface ConfigurationListener {
|
||||||
|
val detectionMethod: Flow<WifiDetectionMethod>
|
||||||
|
val rootShell: RootShell
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val LOCATION_GRANTED = "LOCATION_PERMISSIONS_GRANTED"
|
const val LOCATION_GRANTED = "LOCATION_PERMISSIONS_GRANTED"
|
||||||
const val LOCATION_SERVICES_FILTER = "android.location.PROVIDERS_CHANGED"
|
const val LOCATION_SERVICES_FILTER = "android.location.PROVIDERS_CHANGED"
|
||||||
|
const val ANDROID_UNKNOWN_SSID = "<unknown ssid>"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class WifiDetectionMethod(val value: Int) {
|
enum class WifiDetectionMethod(val value: Int) {
|
||||||
@@ -45,14 +49,12 @@ class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val appContext = context.applicationContext
|
|
||||||
private val packageName = appContext.packageName
|
private val packageName = appContext.packageName
|
||||||
private val connectivityManager =
|
private val connectivityManager =
|
||||||
appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager?
|
private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager?
|
||||||
private val locationManager =
|
private val locationManager =
|
||||||
appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||||
private val rootShell = RootShell(context)
|
|
||||||
|
|
||||||
private val wifiMutex = Mutex()
|
private val wifiMutex = Mutex()
|
||||||
|
|
||||||
@@ -74,16 +76,26 @@ class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetec
|
|||||||
|
|
||||||
data class TransportState(val connected: Boolean = false)
|
data class TransportState(val connected: Boolean = false)
|
||||||
|
|
||||||
private val wifiFlow: Flow<WifiState> = callbackFlow {
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private val wifiFlow: Flow<WifiState> =
|
||||||
|
configurationListener.detectionMethod.flatMapLatest { detectionMethod
|
||||||
|
-> // cancels previous flow
|
||||||
|
createWifiNetworkCallbackFlow(detectionMethod) // Create a new flow for each new method
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createWifiNetworkCallbackFlow(
|
||||||
|
detectionMethod: WifiDetectionMethod
|
||||||
|
): Flow<WifiState> = callbackFlow {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
suspend fun getWifiSsid(): String? {
|
suspend fun getWifiSsid(): String {
|
||||||
return withContext(ioDispatcher) {
|
return withContext(ioDispatcher) {
|
||||||
if (wifiManager == null) return@withContext null
|
if (wifiManager == null) return@withContext ANDROID_UNKNOWN_SSID
|
||||||
try {
|
try {
|
||||||
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() }
|
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() }
|
||||||
|
?: ANDROID_UNKNOWN_SSID
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
null
|
ANDROID_UNKNOWN_SSID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,7 +105,7 @@ class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetec
|
|||||||
val newSsid = getWifiSsid()
|
val newSsid = getWifiSsid()
|
||||||
val securityType = wifiManager?.getCurrentSecurityType()
|
val securityType = wifiManager?.getCurrentSecurityType()
|
||||||
// Only update if new SSID is valid; preserve existing valid SSID otherwise
|
// Only update if new SSID is valid; preserve existing valid SSID otherwise
|
||||||
if (newSsid != null && newSsid != WifiManager.UNKNOWN_SSID) {
|
if (newSsid != WifiManager.UNKNOWN_SSID) {
|
||||||
currentSsid = newSsid
|
currentSsid = newSsid
|
||||||
trySend(WifiState(wifiConnected, currentSsid, securityType))
|
trySend(WifiState(wifiConnected, currentSsid, securityType))
|
||||||
} else if (currentSsid == null || currentSsid == WifiManager.UNKNOWN_SSID) {
|
} else if (currentSsid == null || currentSsid == WifiManager.UNKNOWN_SSID) {
|
||||||
@@ -113,7 +125,7 @@ class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetec
|
|||||||
Timber.d(
|
Timber.d(
|
||||||
"Received update: Precise and all-the-time location permissions are enabled"
|
"Received update: Precise and all-the-time location permissions are enabled"
|
||||||
)
|
)
|
||||||
launch { handleUnknownWifi() }
|
applicationScope.launch { handleUnknownWifi() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +142,8 @@ class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetec
|
|||||||
Timber.d(
|
Timber.d(
|
||||||
"Location Services state changed. Enabled: $isLocationServicesEnabled, GPS: $isGpsEnabled, Network: $isNetworkEnabled"
|
"Location Services state changed. Enabled: $isLocationServicesEnabled, GPS: $isGpsEnabled, Network: $isNetworkEnabled"
|
||||||
)
|
)
|
||||||
if (isLocationServicesEnabled) launch { handleUnknownWifi() }
|
if (isLocationServicesEnabled)
|
||||||
|
applicationScope.launch { handleUnknownWifi() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,22 +193,24 @@ class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetec
|
|||||||
Timber.d("Wi-Fi onAvailable: network=$network")
|
Timber.d("Wi-Fi onAvailable: network=$network")
|
||||||
activeWifiNetworks.add(network.toString())
|
activeWifiNetworks.add(network.toString())
|
||||||
currentSsid =
|
currentSsid =
|
||||||
when (wifiDetectionMethod) {
|
try {
|
||||||
WifiDetectionMethod.DEFAULT -> {
|
when (detectionMethod) {
|
||||||
if (networkCapabilities == null) {
|
WifiDetectionMethod.DEFAULT ->
|
||||||
Timber.d("Capabilities not available, getting SSID via legacy API")
|
networkCapabilities?.getWifiSsid() ?: getWifiSsid()
|
||||||
getWifiSsid()
|
WifiDetectionMethod.LEGACY -> getWifiSsid()
|
||||||
} else
|
WifiDetectionMethod.ROOT ->
|
||||||
networkCapabilities.getWifiSsid().also {
|
configurationListener.rootShell.getCurrentWifiName()
|
||||||
Timber.d("Got SSID from capabilities")
|
WifiDetectionMethod.SHIZUKU ->
|
||||||
|
ShizukuShell(applicationScope)
|
||||||
|
.singleResponseCommand(WIFI_SSID_SHELL_COMMAND)
|
||||||
}
|
}
|
||||||
|
.trim()
|
||||||
|
.replace(Regex("[\n\r]"), "")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
ANDROID_UNKNOWN_SSID
|
||||||
}
|
}
|
||||||
|
.also { Timber.d("Current SSID via ${detectionMethod.name}: $it") }
|
||||||
WifiDetectionMethod.LEGACY -> getWifiSsid()
|
|
||||||
WifiDetectionMethod.ROOT -> rootShell.getCurrentWifiName()
|
|
||||||
// TODO implement Shizuku
|
|
||||||
else -> networkCapabilities?.getWifiSsid() ?: getWifiSsid()
|
|
||||||
}
|
|
||||||
securityType = wifiManager?.getCurrentSecurityType()
|
securityType = wifiManager?.getCurrentSecurityType()
|
||||||
wifiConnected = true
|
wifiConnected = true
|
||||||
trySend(
|
trySend(
|
||||||
@@ -206,30 +221,37 @@ class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetec
|
|||||||
|
|
||||||
val callback =
|
val callback =
|
||||||
when {
|
when {
|
||||||
wifiDetectionMethod == WifiDetectionMethod.LEGACY ||
|
detectionMethod == WifiDetectionMethod.LEGACY ||
|
||||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
|
||||||
object : ConnectivityManager.NetworkCallback() {
|
object : ConnectivityManager.NetworkCallback() {
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
Timber.i("Wi-Fi change detected using old API")
|
applicationScope.launch { handleOnWifiAvailable(network, null) }
|
||||||
launch { handleOnWifiAvailable(network, null) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
override fun onLost(network: Network) {
|
||||||
launch { handleOnWifiLost(network) }
|
applicationScope.launch { handleOnWifiLost(network) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else ->
|
else ->
|
||||||
object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
|
object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
|
||||||
|
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
if (detectionMethod != WifiDetectionMethod.DEFAULT)
|
||||||
|
applicationScope.launch { handleOnWifiAvailable(network, null) }
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCapabilitiesChanged(
|
override fun onCapabilitiesChanged(
|
||||||
network: Network,
|
network: Network,
|
||||||
networkCapabilities: NetworkCapabilities,
|
networkCapabilities: NetworkCapabilities,
|
||||||
) {
|
) {
|
||||||
launch { handleOnWifiAvailable(network, networkCapabilities) }
|
if (detectionMethod == WifiDetectionMethod.DEFAULT)
|
||||||
|
applicationScope.launch {
|
||||||
|
handleOnWifiAvailable(network, networkCapabilities)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
override fun onLost(network: Network) {
|
||||||
launch { handleOnWifiLost(network) }
|
applicationScope.launch { handleOnWifiLost(network) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,7 +266,14 @@ class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetec
|
|||||||
trySend(WifiState())
|
trySend(WifiState())
|
||||||
|
|
||||||
awaitClose {
|
awaitClose {
|
||||||
connectivityManager.unregisterNetworkCallback(callback)
|
try {
|
||||||
|
connectivityManager.unregisterNetworkCallback(callback)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
Timber.e(
|
||||||
|
e,
|
||||||
|
"Flow failed to unregister NetworkCallback, was already unregistered or not registered correctly.",
|
||||||
|
)
|
||||||
|
}
|
||||||
appContext.unregisterReceiver(locationPermissionReceiver)
|
appContext.unregisterReceiver(locationPermissionReceiver)
|
||||||
appContext.unregisterReceiver(locationServicesReceiver)
|
appContext.unregisterReceiver(locationServicesReceiver)
|
||||||
}
|
}
|
||||||
@@ -319,6 +348,7 @@ class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetec
|
|||||||
.also { Timber.d("NetworkStatus: $it") }
|
.also { Timber.d("NetworkStatus: $it") }
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
.shareIn(applicationScope, SharingStarted.WhileSubscribed(5000), replay = 1)
|
||||||
|
|
||||||
override fun sendLocationPermissionsGrantedBroadcast() {
|
override fun sendLocationPermissionsGrantedBroadcast() {
|
||||||
val action = "$packageName.$LOCATION_GRANTED"
|
val action = "$packageName.$LOCATION_GRANTED"
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import android.net.wifi.WifiInfo
|
|||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.wireguard.android.util.RootShell
|
import com.wireguard.android.util.RootShell
|
||||||
|
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor.Companion.ANDROID_UNKNOWN_SSID
|
||||||
|
|
||||||
fun RootShell.getCurrentWifiName(): String? {
|
const val WIFI_SSID_SHELL_COMMAND =
|
||||||
|
"dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: \"[^\"]*\"' | cut -d '\"' -f2"
|
||||||
|
|
||||||
|
fun RootShell.getCurrentWifiName(): String {
|
||||||
val response = mutableListOf<String>()
|
val response = mutableListOf<String>()
|
||||||
this.run(
|
run(response, WIFI_SSID_SHELL_COMMAND)
|
||||||
response,
|
return response.firstOrNull() ?: ANDROID_UNKNOWN_SSID
|
||||||
"dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: [^,]*' | cut -d ' ' -f2- | tr -d '\"'",
|
|
||||||
)
|
|
||||||
return response.firstOrNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@@ -24,7 +25,7 @@ fun WifiManager.getCurrentSecurityType(): WifiSecurityType? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NetworkCapabilities.getWifiSsid(): String? {
|
fun NetworkCapabilities.getWifiSsid(): String {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
val info: WifiInfo
|
val info: WifiInfo
|
||||||
if (transportInfo is WifiInfo) {
|
if (transportInfo is WifiInfo) {
|
||||||
@@ -32,5 +33,5 @@ fun NetworkCapabilities.getWifiSsid(): String? {
|
|||||||
return info.ssid.removeSurrounding("\"").trim()
|
return info.ssid.removeSurrounding("\"").trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return ANDROID_UNKNOWN_SSID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.zaneschepke.networkmonitor.shizuku
|
||||||
|
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import moe.shizuku.server.IRemoteProcess
|
||||||
|
import moe.shizuku.server.IShizukuService
|
||||||
|
import rikka.shizuku.Shizuku
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class ShizukuShell(private val applicationScope: CoroutineScope) {
|
||||||
|
interface CommandResultListener {
|
||||||
|
/*
|
||||||
|
* Runs after the command executes, at least partially. Does not run with 'done' if the command throws an error.
|
||||||
|
* output: The output of the command
|
||||||
|
* done: If the command has finished executing
|
||||||
|
*/
|
||||||
|
fun onCommandResult(output: String, done: Boolean) {}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Runs if the command throws an error.
|
||||||
|
* error: The error message
|
||||||
|
*/
|
||||||
|
fun onCommandError(error: String) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun command(command: String, listener: CommandResultListener, lineBundle: Int = 50) {
|
||||||
|
applicationScope.launch {
|
||||||
|
var process: IRemoteProcess? = null
|
||||||
|
var inputStreamPfd: ParcelFileDescriptor? = null
|
||||||
|
var errorStreamPfd: ParcelFileDescriptor? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
process =
|
||||||
|
IShizukuService.Stub.asInterface(Shizuku.getBinder())
|
||||||
|
.newProcess(arrayOf("sh", "-c", command), null, null)
|
||||||
|
inputStreamPfd = process.inputStream
|
||||||
|
errorStreamPfd = process.errorStream
|
||||||
|
|
||||||
|
FileInputStream(inputStreamPfd.fileDescriptor).use { inputStream ->
|
||||||
|
FileInputStream(errorStreamPfd.fileDescriptor).use { errorStream ->
|
||||||
|
BufferedReader(InputStreamReader(inputStream)).use { reader ->
|
||||||
|
BufferedReader(InputStreamReader(errorStream)).use { err ->
|
||||||
|
val output = StringBuilder()
|
||||||
|
val errorData = StringBuilder()
|
||||||
|
var line: String?
|
||||||
|
var lineCount = 0
|
||||||
|
|
||||||
|
// Read output first
|
||||||
|
while (reader.readLine().also { line = it } != null) {
|
||||||
|
lineCount++
|
||||||
|
output.append(line).append("\n")
|
||||||
|
if (lineCount == lineBundle) {
|
||||||
|
lineCount = 0
|
||||||
|
listener.onCommandResult(
|
||||||
|
output.toString().trim().replace(Regex("[\n\r]"), ""),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
output.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Send any remaining buffered output
|
||||||
|
if (output.isNotBlank()) {
|
||||||
|
listener.onCommandResult(
|
||||||
|
output.toString().trim().replace(Regex("[\n\r]"), ""),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read error stream
|
||||||
|
while (err.readLine().also { line = it } != null) {
|
||||||
|
errorData.append(line).append("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorData.isNotBlank()) {
|
||||||
|
listener.onCommandError(
|
||||||
|
errorData.toString().trim().replace(Regex("[\n\r]"), "")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listener.onCommandResult(
|
||||||
|
output.toString().trim().replace(Regex("[\n\r]"), ""),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the process to complete
|
||||||
|
process.waitFor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "ShizukuShell command failed for command: $command")
|
||||||
|
listener.onCommandError(e.message ?: "Unknown Shizuku command error")
|
||||||
|
} finally {
|
||||||
|
inputStreamPfd?.close()
|
||||||
|
errorStreamPfd?.close()
|
||||||
|
process?.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun singleResponseCommand(command: String) =
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
command(
|
||||||
|
command,
|
||||||
|
object : CommandResultListener {
|
||||||
|
override fun onCommandResult(output: String, done: Boolean) {
|
||||||
|
if (done) continuation.resumeWith(Result.success(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCommandError(error: String) {
|
||||||
|
continuation.resumeWithException(Exception(error))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user