Compare commits

...

4 Commits

Author SHA1 Message Date
Zane Schepke cf8d9c6e4b remove provider 2025-07-05 20:46:46 -04:00
Zane Schepke 1892b9b4a2 make flow hot for flatmap 2025-07-05 15:33:48 -04:00
Zane Schepke 0f246bf066 fix: improve shizuku permission handling 2025-07-05 13:44:51 -04:00
Zane Schepke 0b87d4f23e add init shizuku support 2025-07-05 07:18:59 -04:00
16 changed files with 272 additions and 89 deletions
+4
View File
@@ -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(
@@ -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
@@ -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,
) )
} }
@@ -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))
@@ -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>) =
+1
View File
@@ -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>
+1 -1
View File
@@ -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
View File
@@ -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" }
+4
View File
@@ -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>
@@ -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))
}
},
)
}
}