mirror of
https://github.com/Hamza417/Inure.git
synced 2026-03-13 10:19:43 +08:00
improve: validate invalid permission state #467
This commit is contained in:
@@ -151,6 +151,17 @@ class AdapterPermissions(private val permissions: MutableList<PermissionInfo>, p
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the adapter's data with new permissions list
|
||||
* This is used when the entire list needs to be refreshed (e.g., after search)
|
||||
*/
|
||||
@Suppress("NotifyDataSetChanged")
|
||||
fun updateData(newPermissions: MutableList<PermissionInfo>, @Suppress("UNUSED_PARAMETER") newKeyword: String) {
|
||||
permissions.clear()
|
||||
permissions.addAll(newPermissions)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun update() {
|
||||
permissionLabelMode = PermissionPreferences.getLabelType()
|
||||
for (i in permissions.indices) notifyItemChanged(i)
|
||||
@@ -160,7 +171,7 @@ class AdapterPermissions(private val permissions: MutableList<PermissionInfo>, p
|
||||
this.permissionCallbacks = permissionCallbacks
|
||||
}
|
||||
|
||||
inner class Holder(itemView: View) : VerticalListViewHolder(itemView) {
|
||||
class Holder(itemView: View) : VerticalListViewHolder(itemView) {
|
||||
val name: TypeFaceTextView = itemView.findViewById(R.id.adapter_permissions_name)
|
||||
val status: TypeFaceTextView = itemView.findViewById(R.id.adapter_permissions_status)
|
||||
val desc: TypeFaceTextView = itemView.findViewById(R.id.adapter_permissions_desc)
|
||||
|
||||
@@ -28,6 +28,7 @@ import app.simple.inure.viewmodels.viewers.PermissionsViewModel
|
||||
import com.anggrayudi.storage.extension.postToUi
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import rikka.shizuku.Shizuku
|
||||
@@ -64,77 +65,119 @@ class Permissions : SearchBarScopedFragment() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
permissionsViewModel.getPermissions().observe(viewLifecycleOwner) { permissionInfos ->
|
||||
adapterPermissions = AdapterPermissions(permissionInfos, searchBox.text.toString().trim(), isPackageInstalled)
|
||||
setCount(permissionInfos.size)
|
||||
|
||||
adapterPermissions.setOnPermissionCallbacksListener(object : AdapterPermissions.Companion.PermissionCallbacks {
|
||||
override fun onPermissionClicked(container: View, permissionInfo: PermissionInfo, position: Int) {
|
||||
childFragmentManager.showPermissionStatus(packageInfo, permissionInfo)
|
||||
.setOnPermissionStatusCallbackListener(object : PermissionStatus.Companion.PermissionStatusCallbacks {
|
||||
override fun onSuccess(grantedStatus: Boolean) {
|
||||
adapterPermissions.permissionStatusChanged(position, if (grantedStatus) 1 else 0)
|
||||
}
|
||||
})
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
permissionsViewModel.permissions.collectLatest { permissionInfos ->
|
||||
if (permissionInfos.isEmpty() && !::adapterPermissions.isInitialized) {
|
||||
return@collectLatest
|
||||
}
|
||||
|
||||
override fun onPermissionSwitchClicked(checked: Boolean, permissionInfo: PermissionInfo, position: Int) {
|
||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val mode = if (checked) "grant" else "revoke"
|
||||
if (!::adapterPermissions.isInitialized) {
|
||||
adapterPermissions = AdapterPermissions(permissionInfos, searchBox.text.toString().trim(), isPackageInstalled)
|
||||
|
||||
if (ConfigurationPreferences.isUsingRoot()) {
|
||||
kotlin.runCatching {
|
||||
Shell.cmd("pm $mode ${packageInfo.packageName} ${permissionInfo.name}").exec().let {
|
||||
if (it.isSuccess) {
|
||||
withContext(Dispatchers.Main) {
|
||||
adapterPermissions.permissionStatusChanged(position, if (permissionInfo.isGranted == 1) 0 else 1)
|
||||
adapterPermissions.setOnPermissionCallbacksListener(object : AdapterPermissions.Companion.PermissionCallbacks {
|
||||
override fun onPermissionClicked(container: View, permissionInfo: PermissionInfo, position: Int) {
|
||||
childFragmentManager.showPermissionStatus(packageInfo, permissionInfo)
|
||||
.setOnPermissionStatusCallbackListener(object : PermissionStatus.Companion.PermissionStatusCallbacks {
|
||||
override fun onSuccess(grantedStatus: Boolean) {
|
||||
// Record the expected change
|
||||
val expectedStatus = if (grantedStatus) 1 else 0
|
||||
permissionsViewModel.recordPermissionChangeRequest(permissionInfo.name, position, expectedStatus)
|
||||
|
||||
// Optimistically update UI
|
||||
adapterPermissions.permissionStatusChanged(position, expectedStatus)
|
||||
|
||||
// Schedule a delayed refresh to verify the change
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
permissionsViewModel.refreshPermissionStatus(permissionInfo.name, position)
|
||||
}
|
||||
} else {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onPermissionSwitchClicked(checked: Boolean, permissionInfo: PermissionInfo, position: Int) {
|
||||
val expectedStatus = if (checked) 1 else 0
|
||||
|
||||
// Record the expected change
|
||||
permissionsViewModel.recordPermissionChangeRequest(permissionInfo.name, position, expectedStatus)
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val mode = if (checked) "grant" else "revoke"
|
||||
|
||||
if (ConfigurationPreferences.isUsingRoot()) {
|
||||
kotlin.runCatching {
|
||||
Shell.cmd("pm $mode ${packageInfo.packageName} ${permissionInfo.name}").exec().let {
|
||||
// Refresh to get actual status
|
||||
permissionsViewModel.refreshPermissionStatus(permissionInfo.name, position)
|
||||
}
|
||||
}.getOrElse {
|
||||
withContext(Dispatchers.Main) {
|
||||
showWarning("ERR: failed to $mode permission", goBack = false)
|
||||
showWarning("failed to acquire root", goBack = false)
|
||||
adapterPermissions.permissionStatusChanged(position, permissionInfo.isGranted)
|
||||
}
|
||||
}
|
||||
} else if (ConfigurationPreferences.isUsingShizuku()) {
|
||||
kotlin.runCatching {
|
||||
if (Shizuku.pingBinder()) {
|
||||
ShizukuServiceHelper.getInstance().getBoundService { shizukuService ->
|
||||
shizukuService.simpleExecute("pm $mode ${packageInfo.packageName} ${permissionInfo.name}").let {
|
||||
// Wait a bit for the system to process
|
||||
Thread.sleep(500)
|
||||
|
||||
// Refresh to get actual status
|
||||
permissionsViewModel.refreshPermissionStatus(permissionInfo.name, position)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
postToUi {
|
||||
showWarning("failed to acquire Shizuku", goBack = false)
|
||||
adapterPermissions.permissionStatusChanged(position, permissionInfo.isGranted)
|
||||
}
|
||||
}
|
||||
}.getOrElse {
|
||||
postToUi {
|
||||
showWarning("failed to acquire Shizuku", goBack = false)
|
||||
adapterPermissions.permissionStatusChanged(position, permissionInfo.isGranted)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.getOrElse {
|
||||
withContext(Dispatchers.Main) {
|
||||
showWarning("ERR: failed to acquire root", goBack = false)
|
||||
adapterPermissions.permissionStatusChanged(position, permissionInfo.isGranted)
|
||||
}
|
||||
}
|
||||
} else if (ConfigurationPreferences.isUsingShizuku()) {
|
||||
kotlin.runCatching {
|
||||
if (Shizuku.pingBinder()) {
|
||||
ShizukuServiceHelper.getInstance().getBoundService { shizukuService ->
|
||||
shizukuService.simpleExecute("pm $mode ${packageInfo.packageName} ${permissionInfo.name}").let {
|
||||
postToUi {
|
||||
if (it.isSuccess) {
|
||||
adapterPermissions.permissionStatusChanged(position, if (permissionInfo.isGranted == 1) 0 else 1)
|
||||
} else {
|
||||
showWarning("ERR: failed to $mode permission", goBack = false)
|
||||
adapterPermissions.permissionStatusChanged(position, permissionInfo.isGranted)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
postToUi {
|
||||
showWarning("ERR: failed to acquire Shizuku", goBack = false)
|
||||
adapterPermissions.permissionStatusChanged(position, permissionInfo.isGranted)
|
||||
}
|
||||
}
|
||||
}.getOrElse {
|
||||
postToUi {
|
||||
showWarning("ERR: failed to acquire Shizuku", goBack = false)
|
||||
adapterPermissions.permissionStatusChanged(position, permissionInfo.isGranted)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
recyclerView.setExclusiveAdapter(adapterPermissions)
|
||||
recyclerView.setExclusiveAdapter(adapterPermissions)
|
||||
} else {
|
||||
// Update existing adapter with new data
|
||||
adapterPermissions.updateData(permissionInfos, searchBox.text.toString().trim())
|
||||
}
|
||||
|
||||
setCount(permissionInfos.size)
|
||||
}
|
||||
}
|
||||
|
||||
// Collect single permission updates
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
permissionsViewModel.singlePermissionUpdate.collect { update ->
|
||||
if (::adapterPermissions.isInitialized) {
|
||||
adapterPermissions.permissionStatusChanged(update.position, update.newStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect permission change results
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
permissionsViewModel.permissionChangeResult.collectLatest { result ->
|
||||
result?.let {
|
||||
if (!it.success) {
|
||||
val expectedStatusText = if (permissionsViewModel.lastPermissionChangeRequest.value?.expectedStatus == 1) "granted" else "revoked"
|
||||
val actualStatusText = if (it.actualStatus == 1) "granted" else "revoked"
|
||||
// Permission change failed - show warning
|
||||
showWarning("Failed to change permission state. Expected: $expectedStatusText, Actual: " +
|
||||
"$actualStatusText. The system maybe disallowing permission change for this app.", goBack = false)
|
||||
}
|
||||
// Clear the result after handling
|
||||
permissionsViewModel.clearPermissionChangeResult()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
permissionsViewModel.getError().observe(viewLifecycleOwner) {
|
||||
|
||||
@@ -4,8 +4,6 @@ import android.app.Application
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.simple.inure.R
|
||||
import app.simple.inure.apk.utils.PackageUtils.isPackageInstalled
|
||||
@@ -16,25 +14,59 @@ import app.simple.inure.models.PermissionInfo
|
||||
import app.simple.inure.preferences.SearchPreferences
|
||||
import app.simple.inure.util.StringUtils.capitalizeFirstLetter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
class PermissionsViewModel(application: Application, val packageInfo: PackageInfo) : WrappedViewModel(application) {
|
||||
|
||||
private val permissions: MutableLiveData<MutableList<PermissionInfo>> by lazy {
|
||||
MutableLiveData<MutableList<PermissionInfo>>().also {
|
||||
if (SearchPreferences.isSearchKeywordModeEnabled()) {
|
||||
Log.d("PermissionsViewModel", "Loading permission data with keyword: ${SearchPreferences.getLastSearchKeyword()}")
|
||||
loadPermissionData(SearchPreferences.getLastSearchKeyword())
|
||||
} else {
|
||||
loadPermissionData("")
|
||||
}
|
||||
private val _permissions = MutableStateFlow<MutableList<PermissionInfo>>(mutableListOf())
|
||||
val permissions: StateFlow<MutableList<PermissionInfo>> = _permissions.asStateFlow()
|
||||
|
||||
// Track the last requested permission change
|
||||
private val _lastPermissionChangeRequest = MutableStateFlow<PermissionChangeRequest?>(null)
|
||||
val lastPermissionChangeRequest: StateFlow<PermissionChangeRequest?> = _lastPermissionChangeRequest.asStateFlow()
|
||||
|
||||
// Track permission change results
|
||||
private val _permissionChangeResult = MutableStateFlow<PermissionChangeResult?>(null)
|
||||
val permissionChangeResult: StateFlow<PermissionChangeResult?> = _permissionChangeResult.asStateFlow()
|
||||
|
||||
// Single permission update event
|
||||
private val _singlePermissionUpdate = MutableSharedFlow<PermissionUpdate>()
|
||||
val singlePermissionUpdate: SharedFlow<PermissionUpdate> = _singlePermissionUpdate.asSharedFlow()
|
||||
|
||||
init {
|
||||
if (SearchPreferences.isSearchKeywordModeEnabled()) {
|
||||
Log.d("PermissionsViewModel", "Loading permission data with keyword: ${SearchPreferences.getLastSearchKeyword()}")
|
||||
loadPermissionData(SearchPreferences.getLastSearchKeyword())
|
||||
} else {
|
||||
loadPermissionData("")
|
||||
}
|
||||
}
|
||||
|
||||
fun getPermissions(): LiveData<MutableList<PermissionInfo>> {
|
||||
return permissions
|
||||
}
|
||||
data class PermissionChangeRequest(
|
||||
val permissionName: String,
|
||||
val position: Int,
|
||||
val expectedStatus: Int, // 0 = revoked, 1 = granted
|
||||
val timestamp: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
data class PermissionChangeResult(
|
||||
val permissionName: String,
|
||||
val position: Int,
|
||||
val success: Boolean,
|
||||
val actualStatus: Int
|
||||
)
|
||||
|
||||
data class PermissionUpdate(
|
||||
val position: Int,
|
||||
val newStatus: Int
|
||||
)
|
||||
|
||||
fun loadPermissionData(keyword: String) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
@@ -115,11 +147,11 @@ class PermissionsViewModel(application: Application, val packageInfo: PackageInf
|
||||
}
|
||||
*/
|
||||
|
||||
this@PermissionsViewModel.permissions.postValue(permissions.apply {
|
||||
_permissions.value = permissions.apply {
|
||||
sortBy {
|
||||
it.name.lowercase(Locale.getDefault())
|
||||
}
|
||||
})
|
||||
}
|
||||
}.getOrElse {
|
||||
if (it is java.lang.NullPointerException) {
|
||||
postWarning(getString(R.string.this_app_doesnt_require_any_permissions))
|
||||
@@ -130,6 +162,69 @@ class PermissionsViewModel(application: Application, val packageInfo: PackageInf
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a permission change request before attempting to change it
|
||||
*/
|
||||
fun recordPermissionChangeRequest(permissionName: String, position: Int, expectedStatus: Int) {
|
||||
_lastPermissionChangeRequest.value = PermissionChangeRequest(
|
||||
permissionName = permissionName,
|
||||
position = position,
|
||||
expectedStatus = expectedStatus
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh a single permission's status and verify if the change was successful
|
||||
*/
|
||||
fun refreshPermissionStatus(permissionName: String, position: Int) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
kotlin.runCatching {
|
||||
if (packageManager.isPackageInstalled(packageInfo.packageName)) {
|
||||
val appPackageInfo = packageManager.getPackageInfo(packageInfo.packageName, PackageManager.GET_PERMISSIONS)!!
|
||||
|
||||
val permissionIndex = appPackageInfo.requestedPermissions?.indexOf(permissionName) ?: -1
|
||||
|
||||
if (permissionIndex != -1) {
|
||||
val actualStatus = if (appPackageInfo.requestedPermissionsFlags!![permissionIndex] and PackageInfo.REQUESTED_PERMISSION_GRANTED != 0) {
|
||||
1 // Granted
|
||||
} else {
|
||||
0 // Revoked
|
||||
}
|
||||
|
||||
// Update the permission in the internal list
|
||||
val currentPermissions = _permissions.value
|
||||
if (position >= 0 && position < currentPermissions.size) {
|
||||
currentPermissions[position].isGranted = actualStatus
|
||||
// Emit single permission update event instead of recreating entire list
|
||||
_singlePermissionUpdate.emit(PermissionUpdate(position, actualStatus))
|
||||
}
|
||||
|
||||
// Check if the change was successful
|
||||
val lastRequest = _lastPermissionChangeRequest.value
|
||||
if (lastRequest != null && lastRequest.permissionName == permissionName) {
|
||||
val success = actualStatus == lastRequest.expectedStatus
|
||||
_permissionChangeResult.value = PermissionChangeResult(
|
||||
permissionName = permissionName,
|
||||
position = position,
|
||||
success = success,
|
||||
actualStatus = actualStatus
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.getOrElse {
|
||||
Log.e("PermissionsViewModel", "Failed to refresh permission status", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the last permission change result
|
||||
*/
|
||||
fun clearPermissionChangeResult() {
|
||||
_permissionChangeResult.value = null
|
||||
}
|
||||
|
||||
private fun isKeywordMatched(keyword: String, name: String, loadLabel: String): Boolean {
|
||||
return name.lowercase().contains(keyword.lowercase()) || loadLabel.lowercase().contains(keyword.lowercase())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user