Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class NetworkControlRepositoryImpl @Inject constructor(
return dataSource.checkCompatibility(subId)
}

override suspend fun requestPermission(method: ControlMethod): Boolean {
return getDataSource(method).requestPermission()
}

override suspend fun getCurrentNetworkMode(subId: Int): NetworkMode? {
return try {
val method = preferencesRepository.getControlMethod()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.supernova.networkswitch.domain.model.NetworkMode

interface NetworkControlDataSource {
suspend fun checkCompatibility(subId: Int): CompatibilityState
suspend fun requestPermission(): Boolean
suspend fun getCurrentNetworkMode(subId: Int): NetworkMode?
suspend fun setNetworkMode(subId: Int, mode: NetworkMode)
fun isConnected(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class RootNetworkControlDataSource @Inject constructor(
}
}

override suspend fun requestPermission(): Boolean = Utils.isRootGranted()

override suspend fun getCurrentNetworkMode(subId: Int): NetworkMode? {
return try {
val controller = getNetworkController()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,45 @@ class ShizukuNetworkControlDataSource @Inject constructor(
}
}

/**
* Show Shizuku's permission dialog and wait for the answer
*/
override suspend fun requestPermission(): Boolean {
try {
if (!Shizuku.pingBinder()) return false
if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) return true
// The user chose "Deny" before: Shizuku won't show the dialog again,
// the app has to be allowed from Shizuku's own app list
if (Shizuku.shouldShowRequestPermissionRationale()) return false
} catch (e: Exception) {
return false
}

return suspendCancellableCoroutine { continuation ->
val listener = object : Shizuku.OnRequestPermissionResultListener {
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
if (requestCode != SHIZUKU_PERMISSION_REQUEST_ID) return
Shizuku.removeRequestPermissionResultListener(this)
if (continuation.isActive) {
continuation.resume(grantResult == PackageManager.PERMISSION_GRANTED)
}
}
}
continuation.invokeOnCancellation {
Shizuku.removeRequestPermissionResultListener(listener)
}
Shizuku.addRequestPermissionResultListener(listener)
try {
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_ID)
} catch (e: Exception) {
Shizuku.removeRequestPermissionResultListener(listener)
if (continuation.isActive) {
continuation.resume(false)
}
}
}
}

override suspend fun getCurrentNetworkMode(subId: Int): NetworkMode? {
return if (ensureServiceBinding()) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ interface NetworkControlRepository {
* Check if network control is compatible with current device/method
*/
suspend fun checkCompatibility(method: ControlMethod): CompatibilityState

/**
* Ask for the permission the control method needs (root or Shizuku)
*/
suspend fun requestPermission(method: ControlMethod): Boolean

/**
* Get current network mode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ class CheckCompatibilityUseCase @Inject constructor(
}
}

class RequestPermissionUseCase @Inject constructor(
private val networkControlRepository: NetworkControlRepository
) {
suspend operator fun invoke(method: ControlMethod): Boolean {
return networkControlRepository.requestPermission(method)
}
}

class ToggleNetworkModeUseCase @Inject constructor(
private val networkControlRepository: NetworkControlRepository,
private val preferencesRepository: PreferencesRepository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import com.supernova.networkswitch.domain.model.NetworkMode
import com.supernova.networkswitch.domain.model.ToggleModeConfig
import com.supernova.networkswitch.domain.usecase.CheckCompatibilityUseCase
import com.supernova.networkswitch.domain.usecase.GetCurrentNetworkModeUseCase
import com.supernova.networkswitch.domain.usecase.RequestPermissionUseCase
import com.supernova.networkswitch.domain.usecase.ToggleNetworkModeUseCase
import com.supernova.networkswitch.domain.usecase.UpdateControlMethodUseCase
import com.supernova.networkswitch.domain.usecase.GetToggleModeConfigUseCase
import com.supernova.networkswitch.domain.repository.PreferencesRepository
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.collectLatest
import javax.inject.Inject
Expand All @@ -28,6 +30,7 @@ class MainViewModel @Inject constructor(
private val toggleNetworkModeUseCase: ToggleNetworkModeUseCase,
private val updateControlMethodUseCase: UpdateControlMethodUseCase,
private val getToggleModeConfigUseCase: GetToggleModeConfigUseCase,
private val requestPermissionUseCase: RequestPermissionUseCase,
private val preferencesRepository: PreferencesRepository
) : ViewModel() {

Expand All @@ -50,6 +53,8 @@ class MainViewModel @Inject constructor(
var compatibilityState by mutableStateOf<CompatibilityState>(CompatibilityState.Pending)
private set

private var retryJob: Job? = null

init {
observeControlMethodPreference()
loadToggleModeConfig()
Expand Down Expand Up @@ -102,10 +107,17 @@ class MainViewModel @Inject constructor(
}

/**
* Retry compatibility check
* Retry compatibility check, asking for the missing permission first
*/
fun retryCompatibilityCheck() {
checkCompatibility()
if (retryJob?.isActive == true) return
retryJob = viewModelScope.launch {
val state = compatibilityState
if (state is CompatibilityState.PermissionDenied && state.method == ControlMethod.SHIZUKU) {
requestPermissionUseCase(state.method)
}
checkCompatibility()
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.supernova.networkswitch.domain.repository.PreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.async
import javax.inject.Inject
Expand All @@ -38,6 +39,8 @@ class SettingsViewModel @Inject constructor(

var shizukuCompatibility by mutableStateOf<CompatibilityState>(CompatibilityState.Pending)
private set

private var retryJob: Job? = null

init {
checkAllCompatibility()
Expand All @@ -50,7 +53,15 @@ class SettingsViewModel @Inject constructor(
}

fun retryCompatibilityCheck() {
checkAllCompatibility()
if (retryJob?.isActive == true) return
retryJob = viewModelScope.launch {
if (shizukuCompatibility is CompatibilityState.PermissionDenied &&
preferencesRepository.getControlMethod() == ControlMethod.SHIZUKU
) {
networkControlRepository.requestPermission(ControlMethod.SHIZUKU)
}
checkAllCompatibility()
}
}

private fun checkAllCompatibility() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,41 @@ class SettingsViewModelTest {
coVerify(exactly = 2) { networkControlRepository.checkCompatibility(ControlMethod.ROOT) }
coVerify(exactly = 2) { networkControlRepository.checkCompatibility(ControlMethod.SHIZUKU) }
}

@Test
fun `retryCompatibilityCheck requests Shizuku permission when it is denied`() = runTest {
coEvery { preferencesRepository.getControlMethod() } returns ControlMethod.SHIZUKU
coEvery { networkControlRepository.checkCompatibility(ControlMethod.SHIZUKU) } returns
CompatibilityState.PermissionDenied(ControlMethod.SHIZUKU)
createViewModel()

viewModel.retryCompatibilityCheck()

coVerify(exactly = 1) { networkControlRepository.requestPermission(ControlMethod.SHIZUKU) }
coVerify(exactly = 2) { networkControlRepository.checkCompatibility(ControlMethod.SHIZUKU) }
}

@Test
fun `retryCompatibilityCheck does not request permission when root is selected`() = runTest {
coEvery { preferencesRepository.getControlMethod() } returns ControlMethod.ROOT
coEvery { networkControlRepository.checkCompatibility(ControlMethod.SHIZUKU) } returns
CompatibilityState.PermissionDenied(ControlMethod.SHIZUKU)
createViewModel()

viewModel.retryCompatibilityCheck()

coVerify(exactly = 0) { networkControlRepository.requestPermission(any()) }
}

@Test
fun `retryCompatibilityCheck does not request permission when Shizuku is compatible`() = runTest {
coEvery { preferencesRepository.getControlMethod() } returns ControlMethod.SHIZUKU
coEvery { networkControlRepository.checkCompatibility(ControlMethod.SHIZUKU) } returns
CompatibilityState.Compatible
createViewModel()

viewModel.retryCompatibilityCheck()

coVerify(exactly = 0) { networkControlRepository.requestPermission(any()) }
}
}