mirror of
https://github.com/wgtunnel/android.git
synced 2026-06-02 00:29:08 +02:00
feat: add shizuku support (#852)
This commit is contained in:
@@ -219,6 +219,10 @@ dependencies {
|
||||
implementation(libs.ktor.client.content.negotiation)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
implementation(libs.slf4j.android)
|
||||
|
||||
// shizuku
|
||||
implementation(libs.shizuku.api)
|
||||
implementation(libs.shizuku.provider)
|
||||
}
|
||||
|
||||
tasks.register<Copy>("copyLicenseeJsonToAssets") {
|
||||
|
||||
@@ -87,6 +87,8 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private var lastLocationPermissionState: Boolean? = null
|
||||
|
||||
val REQUEST_CODE = 123
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge(
|
||||
|
||||
+2
-14
@@ -24,22 +24,10 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
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.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
||||
+2
-15
@@ -29,21 +29,8 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
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 kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
||||
@@ -21,7 +21,9 @@ import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
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.GoBackend
|
||||
import org.amnezia.awg.backend.RootTunnelActionHandler
|
||||
@@ -112,11 +114,22 @@ class TunnelModule {
|
||||
fun provideNetworkMonitor(
|
||||
@ApplicationContext context: Context,
|
||||
settingsRepository: AppSettingRepository,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
@AppShell appShell: RootShell,
|
||||
): NetworkMonitor {
|
||||
val method = runBlocking { settingsRepository.get().wifiDetectionMethod }
|
||||
return AndroidNetworkMonitor(
|
||||
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 (
|
||||
uiState.appSettings.wifiDetectionMethod ==
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.ROOT ||
|
||||
uiState.appSettings.wifiDetectionMethod ==
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU ||
|
||||
isWifiNameReadable()
|
||||
) {
|
||||
viewModel.handleEvent(AppEvent.SaveTrustedSSID(ssid))
|
||||
|
||||
-2
@@ -28,8 +28,6 @@ fun WifiDetectionMethodScreen(uiState: AppUiState, viewModel: AppViewModel) {
|
||||
enumValues<AndroidNetworkMonitor.WifiDetectionMethod>().forEach {
|
||||
val title = it.asString(context)
|
||||
val description = it.asDescriptionString(context)
|
||||
// TODO skip shizuku for now
|
||||
if (it == AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU) return@forEach
|
||||
IconSurfaceButton(
|
||||
title = title,
|
||||
onClick = { viewModel.handleEvent(AppEvent.SetDetectionMethod(it)) },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.zaneschepke.wireguardautotunnel.viewmodel
|
||||
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.ViewModel
|
||||
@@ -46,6 +47,7 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.amnezia.awg.config.BadConfigException
|
||||
import org.amnezia.awg.config.Config
|
||||
import rikka.shizuku.Shizuku
|
||||
import timber.log.Timber
|
||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
||||
|
||||
@@ -223,16 +225,31 @@ constructor(
|
||||
) {
|
||||
if (detectionMethod == appSettings.wifiDetectionMethod) return
|
||||
when (detectionMethod) {
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.ROOT -> {
|
||||
val allowed = requestRoot()
|
||||
if (!allowed) return
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.ROOT -> if (!requestRoot()) return
|
||||
AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU -> {
|
||||
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
|
||||
}
|
||||
saveSettings(appSettings.copy(wifiDetectionMethod = detectionMethod))
|
||||
handleShowMessage(StringValue.StringResource(R.string.app_restart_required))
|
||||
}
|
||||
|
||||
private fun handleToggleSelectAllTunnels(tunnels: List<TunnelConf>) =
|
||||
|
||||
@@ -280,4 +280,5 @@
|
||||
Android version
|
||||
</string>
|
||||
<string name="release_notes">Release notes</string>
|
||||
<string name="shizuku_not_detected">Shizuku not detected</string>
|
||||
</resources>
|
||||
|
||||
@@ -13,5 +13,5 @@ object Constants {
|
||||
const val PRERELEASE = "prerelease"
|
||||
|
||||
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"
|
||||
amneziawgAndroid = "1.4.0"
|
||||
androidx-junit = "1.2.1"
|
||||
shizuku = "13.1.5"
|
||||
appcompat = "1.7.1"
|
||||
biometricKtx = "1.2.0-alpha05"
|
||||
coreKtx = "1.16.0"
|
||||
@@ -100,6 +101,8 @@ material-icons-extended = { module = "androidx.compose.material:material-icons-e
|
||||
|
||||
# util
|
||||
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" }
|
||||
semver4j = { module = "com.vdurmont:semver4j", version.ref = "semver4j" }
|
||||
slf4j-android = { module = "org.slf4j:slf4j-android", version.ref = "slf4jAndroid" }
|
||||
|
||||
@@ -43,5 +43,9 @@ dependencies {
|
||||
|
||||
implementation(libs.tunnel)
|
||||
|
||||
// shizuku
|
||||
implementation(libs.shizuku.api)
|
||||
implementation(libs.shizuku.provider)
|
||||
|
||||
implementation(libs.timber)
|
||||
}
|
||||
|
||||
@@ -7,5 +7,15 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"
|
||||
tools:targetApi="29" />
|
||||
<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>
|
||||
|
||||
|
||||
+70
-40
@@ -12,25 +12,29 @@ import android.net.NetworkRequest
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import com.wireguard.android.util.RootShell
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import com.zaneschepke.networkmonitor.shizuku.ShizukuShell
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetectionMethod) :
|
||||
NetworkMonitor {
|
||||
class AndroidNetworkMonitor(
|
||||
private val appContext: Context,
|
||||
private val configurationListener: ConfigurationListener,
|
||||
private val applicationScope: CoroutineScope,
|
||||
) : NetworkMonitor {
|
||||
|
||||
interface ConfigurationListener {
|
||||
val detectionMethod: Flow<WifiDetectionMethod>
|
||||
val rootShell: RootShell
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LOCATION_GRANTED = "LOCATION_PERMISSIONS_GRANTED"
|
||||
const val LOCATION_SERVICES_FILTER = "android.location.PROVIDERS_CHANGED"
|
||||
const val ANDROID_UNKNOWN_SSID = "<unknown ssid>"
|
||||
}
|
||||
|
||||
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 connectivityManager =
|
||||
appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager?
|
||||
private val locationManager =
|
||||
appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
private val rootShell = RootShell(context)
|
||||
|
||||
private val wifiMutex = Mutex()
|
||||
|
||||
@@ -74,16 +76,26 @@ class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetec
|
||||
|
||||
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")
|
||||
suspend fun getWifiSsid(): String? {
|
||||
suspend fun getWifiSsid(): String {
|
||||
return withContext(ioDispatcher) {
|
||||
if (wifiManager == null) return@withContext null
|
||||
if (wifiManager == null) return@withContext ANDROID_UNKNOWN_SSID
|
||||
try {
|
||||
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() }
|
||||
?: ANDROID_UNKNOWN_SSID
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
null
|
||||
ANDROID_UNKNOWN_SSID
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +105,7 @@ class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetec
|
||||
val newSsid = getWifiSsid()
|
||||
val securityType = wifiManager?.getCurrentSecurityType()
|
||||
// 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
|
||||
trySend(WifiState(wifiConnected, currentSsid, securityType))
|
||||
} else if (currentSsid == null || currentSsid == WifiManager.UNKNOWN_SSID) {
|
||||
@@ -113,7 +125,7 @@ class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetec
|
||||
Timber.d(
|
||||
"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(
|
||||
"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")
|
||||
activeWifiNetworks.add(network.toString())
|
||||
currentSsid =
|
||||
when (wifiDetectionMethod) {
|
||||
WifiDetectionMethod.DEFAULT -> {
|
||||
if (networkCapabilities == null) {
|
||||
Timber.d("Capabilities not available, getting SSID via legacy API")
|
||||
getWifiSsid()
|
||||
} else
|
||||
networkCapabilities.getWifiSsid().also {
|
||||
Timber.d("Got SSID from capabilities")
|
||||
try {
|
||||
when (detectionMethod) {
|
||||
WifiDetectionMethod.DEFAULT ->
|
||||
networkCapabilities?.getWifiSsid() ?: getWifiSsid()
|
||||
WifiDetectionMethod.LEGACY -> getWifiSsid()
|
||||
WifiDetectionMethod.ROOT ->
|
||||
configurationListener.rootShell.getCurrentWifiName()
|
||||
WifiDetectionMethod.SHIZUKU ->
|
||||
ShizukuShell(applicationScope)
|
||||
.singleResponseCommand(WIFI_SSID_SHELL_COMMAND)
|
||||
}
|
||||
.trim()
|
||||
.replace(Regex("[\n\r]"), "")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
ANDROID_UNKNOWN_SSID
|
||||
}
|
||||
|
||||
WifiDetectionMethod.LEGACY -> getWifiSsid()
|
||||
WifiDetectionMethod.ROOT -> rootShell.getCurrentWifiName()
|
||||
// TODO implement Shizuku
|
||||
else -> networkCapabilities?.getWifiSsid() ?: getWifiSsid()
|
||||
}
|
||||
.also { Timber.d("Current SSID via ${detectionMethod.name}: $it") }
|
||||
securityType = wifiManager?.getCurrentSecurityType()
|
||||
wifiConnected = true
|
||||
trySend(
|
||||
@@ -206,30 +221,37 @@ class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetec
|
||||
|
||||
val callback =
|
||||
when {
|
||||
wifiDetectionMethod == WifiDetectionMethod.LEGACY ||
|
||||
detectionMethod == WifiDetectionMethod.LEGACY ||
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
Timber.i("Wi-Fi change detected using old API")
|
||||
launch { handleOnWifiAvailable(network, null) }
|
||||
applicationScope.launch { handleOnWifiAvailable(network, null) }
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
launch { handleOnWifiLost(network) }
|
||||
applicationScope.launch { handleOnWifiLost(network) }
|
||||
}
|
||||
}
|
||||
else ->
|
||||
object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
if (detectionMethod != WifiDetectionMethod.DEFAULT)
|
||||
applicationScope.launch { handleOnWifiAvailable(network, null) }
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities,
|
||||
) {
|
||||
launch { handleOnWifiAvailable(network, networkCapabilities) }
|
||||
if (detectionMethod == WifiDetectionMethod.DEFAULT)
|
||||
applicationScope.launch {
|
||||
handleOnWifiAvailable(network, networkCapabilities)
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
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(locationServicesReceiver)
|
||||
}
|
||||
@@ -319,6 +348,7 @@ class AndroidNetworkMonitor(context: Context, val wifiDetectionMethod: WifiDetec
|
||||
.also { Timber.d("NetworkStatus: $it") }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.shareIn(applicationScope, SharingStarted.WhileSubscribed(5000), replay = 1)
|
||||
|
||||
override fun sendLocationPermissionsGrantedBroadcast() {
|
||||
val action = "$packageName.$LOCATION_GRANTED"
|
||||
|
||||
@@ -5,14 +5,15 @@ import android.net.wifi.WifiInfo
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
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>()
|
||||
this.run(
|
||||
response,
|
||||
"dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: [^,]*' | cut -d ' ' -f2- | tr -d '\"'",
|
||||
)
|
||||
return response.firstOrNull()
|
||||
run(response, WIFI_SSID_SHELL_COMMAND)
|
||||
return response.firstOrNull() ?: ANDROID_UNKNOWN_SSID
|
||||
}
|
||||
|
||||
@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) {
|
||||
val info: WifiInfo
|
||||
if (transportInfo is WifiInfo) {
|
||||
@@ -32,5 +33,5 @@ fun NetworkCapabilities.getWifiSsid(): String? {
|
||||
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