Skip to content

Commit 038fcde

Browse files
committed
improve: validate invalid permission state #467
1 parent 6cd0bc2 commit 038fcde

File tree

3 files changed

+219
-70
lines changed

3 files changed

+219
-70
lines changed

app/src/main/java/app/simple/inure/adapters/viewers/AdapterPermissions.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,17 @@ class AdapterPermissions(private val permissions: MutableList<PermissionInfo>, p
151151
notifyItemChanged(position)
152152
}
153153

154+
/**
155+
* Update the adapter's data with new permissions list
156+
* This is used when the entire list needs to be refreshed (e.g., after search)
157+
*/
158+
@Suppress("NotifyDataSetChanged")
159+
fun updateData(newPermissions: MutableList<PermissionInfo>, @Suppress("UNUSED_PARAMETER") newKeyword: String) {
160+
permissions.clear()
161+
permissions.addAll(newPermissions)
162+
notifyDataSetChanged()
163+
}
164+
154165
fun update() {
155166
permissionLabelMode = PermissionPreferences.getLabelType()
156167
for (i in permissions.indices) notifyItemChanged(i)
@@ -160,7 +171,7 @@ class AdapterPermissions(private val permissions: MutableList<PermissionInfo>, p
160171
this.permissionCallbacks = permissionCallbacks
161172
}
162173

163-
inner class Holder(itemView: View) : VerticalListViewHolder(itemView) {
174+
class Holder(itemView: View) : VerticalListViewHolder(itemView) {
164175
val name: TypeFaceTextView = itemView.findViewById(R.id.adapter_permissions_name)
165176
val status: TypeFaceTextView = itemView.findViewById(R.id.adapter_permissions_status)
166177
val desc: TypeFaceTextView = itemView.findViewById(R.id.adapter_permissions_desc)

app/src/main/java/app/simple/inure/ui/viewers/Permissions.kt

Lines changed: 97 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import app.simple.inure.viewmodels.viewers.PermissionsViewModel
2828
import com.anggrayudi.storage.extension.postToUi
2929
import com.topjohnwu.superuser.Shell
3030
import kotlinx.coroutines.Dispatchers
31+
import kotlinx.coroutines.flow.collectLatest
3132
import kotlinx.coroutines.launch
3233
import kotlinx.coroutines.withContext
3334
import rikka.shizuku.Shizuku
@@ -64,77 +65,119 @@ class Permissions : SearchBarScopedFragment() {
6465
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
6566
super.onViewCreated(view, savedInstanceState)
6667

67-
permissionsViewModel.getPermissions().observe(viewLifecycleOwner) { permissionInfos ->
68-
adapterPermissions = AdapterPermissions(permissionInfos, searchBox.text.toString().trim(), isPackageInstalled)
69-
setCount(permissionInfos.size)
70-
71-
adapterPermissions.setOnPermissionCallbacksListener(object : AdapterPermissions.Companion.PermissionCallbacks {
72-
override fun onPermissionClicked(container: View, permissionInfo: PermissionInfo, position: Int) {
73-
childFragmentManager.showPermissionStatus(packageInfo, permissionInfo)
74-
.setOnPermissionStatusCallbackListener(object : PermissionStatus.Companion.PermissionStatusCallbacks {
75-
override fun onSuccess(grantedStatus: Boolean) {
76-
adapterPermissions.permissionStatusChanged(position, if (grantedStatus) 1 else 0)
77-
}
78-
})
68+
viewLifecycleOwner.lifecycleScope.launch {
69+
permissionsViewModel.permissions.collectLatest { permissionInfos ->
70+
if (permissionInfos.isEmpty() && !::adapterPermissions.isInitialized) {
71+
return@collectLatest
7972
}
8073

81-
override fun onPermissionSwitchClicked(checked: Boolean, permissionInfo: PermissionInfo, position: Int) {
82-
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
83-
val mode = if (checked) "grant" else "revoke"
74+
if (!::adapterPermissions.isInitialized) {
75+
adapterPermissions = AdapterPermissions(permissionInfos, searchBox.text.toString().trim(), isPackageInstalled)
8476

85-
if (ConfigurationPreferences.isUsingRoot()) {
86-
kotlin.runCatching {
87-
Shell.cmd("pm $mode ${packageInfo.packageName} ${permissionInfo.name}").exec().let {
88-
if (it.isSuccess) {
89-
withContext(Dispatchers.Main) {
90-
adapterPermissions.permissionStatusChanged(position, if (permissionInfo.isGranted == 1) 0 else 1)
77+
adapterPermissions.setOnPermissionCallbacksListener(object : AdapterPermissions.Companion.PermissionCallbacks {
78+
override fun onPermissionClicked(container: View, permissionInfo: PermissionInfo, position: Int) {
79+
childFragmentManager.showPermissionStatus(packageInfo, permissionInfo)
80+
.setOnPermissionStatusCallbackListener(object : PermissionStatus.Companion.PermissionStatusCallbacks {
81+
override fun onSuccess(grantedStatus: Boolean) {
82+
// Record the expected change
83+
val expectedStatus = if (grantedStatus) 1 else 0
84+
permissionsViewModel.recordPermissionChangeRequest(permissionInfo.name, position, expectedStatus)
85+
86+
// Optimistically update UI
87+
adapterPermissions.permissionStatusChanged(position, expectedStatus)
88+
89+
// Schedule a delayed refresh to verify the change
90+
viewLifecycleOwner.lifecycleScope.launch {
91+
permissionsViewModel.refreshPermissionStatus(permissionInfo.name, position)
9192
}
92-
} else {
93+
}
94+
})
95+
}
96+
97+
override fun onPermissionSwitchClicked(checked: Boolean, permissionInfo: PermissionInfo, position: Int) {
98+
val expectedStatus = if (checked) 1 else 0
99+
100+
// Record the expected change
101+
permissionsViewModel.recordPermissionChangeRequest(permissionInfo.name, position, expectedStatus)
102+
103+
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
104+
val mode = if (checked) "grant" else "revoke"
105+
106+
if (ConfigurationPreferences.isUsingRoot()) {
107+
kotlin.runCatching {
108+
Shell.cmd("pm $mode ${packageInfo.packageName} ${permissionInfo.name}").exec().let {
109+
// Refresh to get actual status
110+
permissionsViewModel.refreshPermissionStatus(permissionInfo.name, position)
111+
}
112+
}.getOrElse {
93113
withContext(Dispatchers.Main) {
94-
showWarning("ERR: failed to $mode permission", goBack = false)
114+
showWarning("failed to acquire root", goBack = false)
95115
adapterPermissions.permissionStatusChanged(position, permissionInfo.isGranted)
96116
}
97117
}
98-
}
99-
}.getOrElse {
100-
withContext(Dispatchers.Main) {
101-
showWarning("ERR: failed to acquire root", goBack = false)
102-
adapterPermissions.permissionStatusChanged(position, permissionInfo.isGranted)
103-
}
104-
}
105-
} else if (ConfigurationPreferences.isUsingShizuku()) {
106-
kotlin.runCatching {
107-
if (Shizuku.pingBinder()) {
108-
ShizukuServiceHelper.getInstance().getBoundService { shizukuService ->
109-
shizukuService.simpleExecute("pm $mode ${packageInfo.packageName} ${permissionInfo.name}").let {
110-
postToUi {
111-
if (it.isSuccess) {
112-
adapterPermissions.permissionStatusChanged(position, if (permissionInfo.isGranted == 1) 0 else 1)
113-
} else {
114-
showWarning("ERR: failed to $mode permission", goBack = false)
115-
adapterPermissions.permissionStatusChanged(position, permissionInfo.isGranted)
118+
} else if (ConfigurationPreferences.isUsingShizuku()) {
119+
kotlin.runCatching {
120+
if (Shizuku.pingBinder()) {
121+
ShizukuServiceHelper.getInstance().getBoundService { shizukuService ->
122+
shizukuService.simpleExecute("pm $mode ${packageInfo.packageName} ${permissionInfo.name}").let {
123+
// Wait a bit for the system to process
124+
Thread.sleep(500)
125+
126+
// Refresh to get actual status
127+
permissionsViewModel.refreshPermissionStatus(permissionInfo.name, position)
116128
}
117129
}
130+
} else {
131+
postToUi {
132+
showWarning("failed to acquire Shizuku", goBack = false)
133+
adapterPermissions.permissionStatusChanged(position, permissionInfo.isGranted)
134+
}
135+
}
136+
}.getOrElse {
137+
postToUi {
138+
showWarning("failed to acquire Shizuku", goBack = false)
139+
adapterPermissions.permissionStatusChanged(position, permissionInfo.isGranted)
118140
}
119141
}
120-
} else {
121-
postToUi {
122-
showWarning("ERR: failed to acquire Shizuku", goBack = false)
123-
adapterPermissions.permissionStatusChanged(position, permissionInfo.isGranted)
124-
}
125-
}
126-
}.getOrElse {
127-
postToUi {
128-
showWarning("ERR: failed to acquire Shizuku", goBack = false)
129-
adapterPermissions.permissionStatusChanged(position, permissionInfo.isGranted)
130142
}
131143
}
132144
}
133-
}
145+
})
146+
147+
recyclerView.setExclusiveAdapter(adapterPermissions)
148+
} else {
149+
// Update existing adapter with new data
150+
adapterPermissions.updateData(permissionInfos, searchBox.text.toString().trim())
134151
}
135-
})
136152

137-
recyclerView.setExclusiveAdapter(adapterPermissions)
153+
setCount(permissionInfos.size)
154+
}
155+
}
156+
157+
// Collect single permission updates
158+
viewLifecycleOwner.lifecycleScope.launch {
159+
permissionsViewModel.singlePermissionUpdate.collect { update ->
160+
if (::adapterPermissions.isInitialized) {
161+
adapterPermissions.permissionStatusChanged(update.position, update.newStatus)
162+
}
163+
}
164+
}
165+
166+
// Collect permission change results
167+
viewLifecycleOwner.lifecycleScope.launch {
168+
permissionsViewModel.permissionChangeResult.collectLatest { result ->
169+
result?.let {
170+
if (!it.success) {
171+
val expectedStatusText = if (permissionsViewModel.lastPermissionChangeRequest.value?.expectedStatus == 1) "granted" else "revoked"
172+
val actualStatusText = if (it.actualStatus == 1) "granted" else "revoked"
173+
// Permission change failed - show warning
174+
showWarning("Failed to change permission state. Expected: $expectedStatusText, Actual: " +
175+
"$actualStatusText. The system maybe disallowing permission change for this app.", goBack = false)
176+
}
177+
// Clear the result after handling
178+
permissionsViewModel.clearPermissionChangeResult()
179+
}
180+
}
138181
}
139182

140183
permissionsViewModel.getError().observe(viewLifecycleOwner) {

0 commit comments

Comments
 (0)