diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4b8ea885..6637546f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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("copyLicenseeJsonToAssets") { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt index 13afbdfd..912acbfe 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt @@ -87,6 +87,8 @@ class MainActivity : AppCompatActivity() { private var lastLocationPermissionState: Boolean? = null + val REQUEST_CODE = 123 + @SuppressLint("BatteryLife") override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/TunnelForegroundService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/TunnelForegroundService.kt index e2df2619..c4d45d86 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/TunnelForegroundService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/TunnelForegroundService.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/autotunnel/AutoTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/autotunnel/AutoTunnelService.kt index ea16c602..aa41629c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/autotunnel/AutoTunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/autotunnel/AutoTunnelService.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/TunnelModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/TunnelModule.kt index 25fe1f91..f89ff906 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/TunnelModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/TunnelModule.kt @@ -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 + get() = + settingsRepository.flow + .distinctUntilChangedBy { it.wifiDetectionMethod } + .map { it.wifiDetectionMethod } + + override val rootShell: RootShell + get() = appShell + }, + applicationScope, ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/WifiTunnelingItems.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/WifiTunnelingItems.kt index e9e6f7ff..d1d7a71d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/WifiTunnelingItems.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/WifiTunnelingItems.kt @@ -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)) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/detection/WifiDetectionMethodScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/detection/WifiDetectionMethodScreen.kt index 00f0bf57..303423d2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/detection/WifiDetectionMethodScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/detection/WifiDetectionMethodScreen.kt @@ -28,8 +28,6 @@ fun WifiDetectionMethodScreen(uiState: AppUiState, viewModel: AppViewModel) { enumValues().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)) }, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/AppViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/AppViewModel.kt index 7a473212..33f2a31d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/AppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/AppViewModel.kt @@ -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) = diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 67fc14bf..86a3a6c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -280,4 +280,5 @@ Android version Release notes + Shizuku not detected diff --git a/buildSrc/src/main/kotlin/Constants.kt b/buildSrc/src/main/kotlin/Constants.kt index bd947d21..3bd0aaa5 100644 --- a/buildSrc/src/main/kotlin/Constants.kt +++ b/buildSrc/src/main/kotlin/Constants.kt @@ -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") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2662196e..0dbbbc91 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/networkmonitor/build.gradle.kts b/networkmonitor/build.gradle.kts index d19cbf4e..ab585c3f 100644 --- a/networkmonitor/build.gradle.kts +++ b/networkmonitor/build.gradle.kts @@ -43,5 +43,9 @@ dependencies { implementation(libs.tunnel) + // shizuku + implementation(libs.shizuku.api) + implementation(libs.shizuku.provider) + implementation(libs.timber) } diff --git a/networkmonitor/src/main/AndroidManifest.xml b/networkmonitor/src/main/AndroidManifest.xml index ca9757bb..4f250668 100644 --- a/networkmonitor/src/main/AndroidManifest.xml +++ b/networkmonitor/src/main/AndroidManifest.xml @@ -7,5 +7,15 @@ + + + + diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt index e5263b45..665eeb2f 100644 --- a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt @@ -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 + 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 = "" } 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 = callbackFlow { + @OptIn(ExperimentalCoroutinesApi::class) + private val wifiFlow: Flow = + configurationListener.detectionMethod.flatMapLatest { detectionMethod + -> // cancels previous flow + createWifiNetworkCallbackFlow(detectionMethod) // Create a new flow for each new method + } + + private fun createWifiNetworkCallbackFlow( + detectionMethod: WifiDetectionMethod + ): Flow = 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" diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/Extensions.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/Extensions.kt index 1bd647ee..06bbbe6f 100644 --- a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/Extensions.kt +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/Extensions.kt @@ -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() - 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 } diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/shizuku/ShizukuShell.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/shizuku/ShizukuShell.kt new file mode 100644 index 00000000..1d777ff3 --- /dev/null +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/shizuku/ShizukuShell.kt @@ -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)) + } + }, + ) + } +}