Skip to content
Closed
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
92 changes: 8 additions & 84 deletions daemon/src/main/kotlin/org/matrix/vector/daemon/VectorService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -467,72 +467,12 @@ object VectorService : IVectorDaemon.Stub() {
}

when (action) {
"approve" -> {
// "system" is the framework and not a package: it names system_server, which belongs
// to no package and resolves for nobody. The lookup below therefore came back null
// for every framework prompt, and the approval the user had just given was answered
// "Package not found" — the request was closed, its notification cancelled, and no
// row written. The normalisation to user 0 further down could never once have run.
//
// Package by package rather than all-or-nothing: the prompt may have been up for an
// hour and one of the packages it named can have been uninstalled in the meantime,
// which is no reason to throw away the user's answer about the rest.
//
// Only under "approve", because only an approval has to name something real. Deny
// and the timeout used to be refused here too, so dismissing a prompt for a package
// that had since been uninstalled told the module "Package not found" when what had
// actually happened was that the user turned it down.
val granted =
scopePackageNames.filter {
it == "system" || packageManager?.getPackageInfoCompat(it, 0, userId) != null
}
if (granted.isEmpty()) {
// Logged, because until now this said nothing anywhere: the module was told
// "Package not found", the user was told nothing at all, and the daemon kept no
// record that the press had even arrived. The framework-scope failure above was
// invisible for exactly that reason.
Log.w(
TAG,
"None of ${scopePackageNames.joinToString()} resolve for user $userId;" +
" refusing the scope request of $packageName")
// Leaving the whole function here skipped the cancel below, which used to be
// merely untidy and is now a prompt nobody can use: the request has been answered,
// so every later press of its buttons is dropped. The request is over either way,
// so the notification goes with it.
iCallback.onScopeRequestFailed("Package not found")
"approve" -> ModuleDatabase.approveModuleScope(packageName, userId, scopePackageNames)
.onSuccess { granted -> iCallback.onScopeRequestApproved(granted) }
.onFailure {
iCallback.onScopeRequestFailed(it.message)
return@runCatching
}
val scopes = ModuleDatabase.getModuleScope(packageName) ?: mutableListOf()
var added = false
granted.forEach { scopePackageName ->
// Compared against where the row will land, not against the user who asked: the
// framework is stored under user 0 whoever requested it, so for "system" this test
// never matched and every approval appended a duplicate and rewrote the whole
// table.
val storedUserId = if (scopePackageName == "system") 0 else userId
val present =
scopes.any { it.packageName == scopePackageName && it.userId == storedUserId }
if (!present) {
scopes.add(
ScopeEntry().apply {
this.packageName = scopePackageName
this.userId = storedUserId
})
added = true
}
}
// One write for the whole prompt, and none at all when the user approved what the
// module already had. `setModuleScope` replaces the module's rows wholesale and
// enables the module on the way through, so writing per package would rewrite the
// table once per package — leaving a window after each in which the scope is only
// partly what was agreed to — and writing unconditionally would let a module enable
// itself by asking again for what it has.
if (added) ModuleDatabase.setModuleScope(packageName, scopes)
Log.i(TAG, "Approved ${granted.joinToString()} for $packageName on user $userId")
// The packages that were granted, which is what the list in this callback is for. A
// module comparing it against what it asked for can see what it did not get.
iCallback.onScopeRequestApproved(granted)
}
"deny" -> iCallback.onScopeRequestFailed("Request denied by user")
"delete" -> iCallback.onScopeRequestFailed("Request timeout")
}
Expand All @@ -545,23 +485,8 @@ object VectorService : IVectorDaemon.Stub() {
NotificationManager.cancelScopeRequest(packageName, userId, scopePackageNames)
}

/**
* The modules that may not ask for scope again.
*
* Filed under "lspd" rather than under the module it names, because it records the user's decision
* about a module rather than that module's own configuration. That is also why uninstalling a
* module does not take it away — `deleteModulePrefs` deletes what is filed under the module's own
* name — and why [unblockScopeRequests] has to exist.
*/
@Suppress("UNCHECKED_CAST")
private fun blockedScopeRequests(): Set<String> =
PreferenceStore.getModulePrefs("lspd", 0, "config")["scope_request_blocked"] as? Set<String>
?: emptySet()

private fun blockScopeRequests(packageName: String) {
PreferenceStore.updateModulePref(
"lspd", 0, "config", "scope_request_blocked", blockedScopeRequests() + packageName)
}
private fun blockScopeRequests(packageName: String) =
PreferenceStore.setBlockedScopeRequests(PreferenceStore.getBlockedScopeRequests() + packageName)

/**
* Lets an uninstalled module ask again if it comes back.
Expand All @@ -573,10 +498,9 @@ object VectorService : IVectorDaemon.Stub() {
* back — and a module that is gone has no decision left to honour.
*/
private fun unblockScopeRequests(packageName: String) {
val blocked = blockedScopeRequests()
val blocked = PreferenceStore.getBlockedScopeRequests()
if (packageName !in blocked) return
PreferenceStore.updateModulePref(
"lspd", 0, "config", "scope_request_blocked", blocked - packageName)
PreferenceStore.setBlockedScopeRequests(blocked - packageName)
Log.i(TAG, "$packageName was uninstalled; it may ask for scope again if it returns")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package org.matrix.vector.daemon.data
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import android.util.Log
import org.matrix.vector.ipc.ScopeEntry
import org.matrix.vector.daemon.system.NotificationManager
import org.matrix.vector.daemon.system.getPackageInfoCompat
import org.matrix.vector.daemon.system.packageManager
import org.matrix.vector.ipc.ScopeEntry

private const val TAG = "VectorModuleDatabase"

Expand Down Expand Up @@ -403,4 +405,71 @@ object ModuleDatabase {

return changed
}

fun approveModuleScope(packageName: String, userId: Int, requested: List<String>): Result<List<String>> {
// "system" is the framework and not a package: it names system_server, which belongs
// to no package and resolves for nobody. The lookup below therefore came back null
// for every framework prompt, and the approval the user had just given was answered
// "Package not found" — the request was closed, its notification canceled, and no
// row written. The normalization to user 0 further down could never once have run.
//
// Package by package rather than all-or-nothing: the prompt may have been up for an
// hour and one of the packages it named can have been uninstalled in the meantime,
// which is no reason to throw away the user's answer about the rest.
//
// Only under "approve", because only an approval has to name something real. Deny
// and the timeout used to be refused here too, so dismissing a prompt for a package
// that had since been uninstalled told the module "Package not found" when what had
// actually happened was that the user turned it down.
val granted =
requested.filter {
it == "system" || packageManager?.getPackageInfoCompat(it, 0, userId) != null
}
if (granted.isEmpty()) {
// Logged, because until now this said nothing anywhere: the module was told
// "Package not found", the user was told nothing at all, and the daemon kept no
// record that the press had even arrived. The framework-scope failure above was
// invisible for exactly that reason.
Log.w(
TAG,
"None of ${requested.joinToString()} resolve for user $userId;" +
" refusing the scope request of $packageName"
)
// Leaving the whole function here skipped the cancel below, which used to be
// merely untidy and is now a prompt nobody can use: the request has been answered,
// so every later press of its buttons is dropped. The request is over either way,
// so the notification goes with it.
return Result.failure(Exception("Package not found"))
}
val scopes = getModuleScope(packageName) ?: mutableListOf()
var added = false
granted.forEach { scopePackageName ->
// Compared against where the row will land, not against the user who asked: the
// framework is stored under user 0 whoever requested it, so for "system" this test
// never matched and every approval appended a duplicate and rewrote the whole
// table.
val storedUserId = if (scopePackageName == "system") 0 else userId
val present =
scopes.any { it.packageName == scopePackageName && it.userId == storedUserId }
if (!present) {
scopes.add(
ScopeEntry().apply {
this.packageName = scopePackageName
this.userId = storedUserId
})
added = true
}
}
// One write for the whole prompt, and none at all when the user approved what the
// module already had. `setModuleScope` replaces the module's rows wholesale and
// enables the module on the way through, so writing per package would rewrite the
// table once per package — leaving a window after each in which the scope is only
// partly what was agreed to — and writing unconditionally would let a module enable
// itself by asking again for what it has.
if (added) setModuleScope(packageName, scopes)
Log.i(TAG, "Approved ${granted.joinToString()} for $packageName on user $userId")
// The packages that were granted, which is what the list in this callback is for. A
// module comparing it against what it asked for can see what it did not get.
return Result.success(granted)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,23 @@ object PreferenceStore {
fun setVerboseLog(enabled: Boolean) =
updateModulePref("lspd", 0, "config", "enable_verbose_log", enabled)

@Suppress("UNCHECKED_CAST")
fun getBlockedScopeRequests(): Set<String> =
getModulePrefs("lspd", 0, "config")["scope_request_blocked"] as? Set<String> ?: emptySet()

fun setBlockedScopeRequests(scopes: Set<String>) =
updateModulePref("lspd", 0, "config", "scope_request_blocked", scopes)

fun isScopeRequestBlocked(pkg: String): Boolean =
(getModulePrefs("lspd", 0, "config")["scope_request_blocked"] as? Set<*>)?.contains(pkg) ==
true
getBlockedScopeRequests().contains(pkg)

@Suppress("UNCHECKED_CAST")
fun getApprovedScopeRequests(): Set<String> =
getModulePrefs("lspd", 0, "config")["scope_request_approved"] as? Set<String> ?: emptySet()

fun setApprovedScopeRequests(scopes: Set<String>) =
updateModulePref("lspd", 0, "config", "scope_request_approved", scopes)

fun isScopeRequestApproved(pkg: String): Boolean =
getApprovedScopeRequests().contains(pkg)
}
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,24 @@ object ManagerService : IManagerService.Stub() {

override fun getModuleScope(packageName: String) = ModuleDatabase.getModuleScope(packageName)

override fun isScopeRequestBlocked(packageName: String) =
PreferenceStore.isScopeRequestBlocked(packageName)

override fun setModuleScopeRequestBlocked(packageName: String, block: Boolean) {
val blocked = PreferenceStore.getBlockedScopeRequests()
if (block xor (packageName !in blocked)) return
PreferenceStore.setBlockedScopeRequests(if (block) blocked + packageName else blocked - packageName)
}

override fun isScopeRequestApproved(packageName: String) =
PreferenceStore.isScopeRequestApproved(packageName)

override fun setModuleScopeRequestApproved(packageName: String, approve: Boolean) {
val approved = PreferenceStore.getApprovedScopeRequests()
if (approve xor (packageName !in approved)) return
PreferenceStore.setApprovedScopeRequests(if (approve) approved + packageName else approved - packageName)
}

// Reports the setting, not the setting OR'd with the build type. It used to be
// `|| BuildConfig.DEBUG`, which made the value unwritable on a debug daemon: the manager could
// never read false, so its switch snapped back on every tap and had to be greyed out. The OR was
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,11 @@ class ModuleAppService(private val loadedModule: LoadedModule) : IXposedService.
return
}
}
if (!PreferenceStore.isScopeRequestBlocked(loadedModule.packageName)) {
if (PreferenceStore.isScopeRequestApproved(loadedModule.packageName)) {
ModuleDatabase.approveModuleScope(loadedModule.packageName, userId, requested)
.onSuccess { granted -> callback.onScopeRequestApproved(granted) }
.onFailure { callback.onScopeRequestFailed(it.message) }
} else if (!PreferenceStore.isScopeRequestBlocked(loadedModule.packageName)) {
NotificationManager.requestModuleScope(loadedModule.packageName, userId, requested, callback)
} else {
callback.onScopeRequestFailed("Scope request blocked by user configuration")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,20 @@ class FakeManagerService(
override fun getModuleScope(packageName: String?): MutableList<ScopeEntry>? =
if (real == null) mutableListOf() else real.getModuleScope(packageName)

override fun isScopeRequestBlocked(packageName: String?): Boolean =
real?.isScopeRequestBlocked(packageName) ?: false

override fun setModuleScopeRequestBlocked(packageName: String?, block: Boolean) {
real?.setModuleScopeRequestBlocked(packageName, block)
}

override fun isScopeRequestApproved(packageName: String?): Boolean =
real?.isScopeRequestApproved(packageName) ?: false

override fun setModuleScopeRequestApproved(packageName: String?, approve: Boolean) {
real?.setModuleScopeRequestApproved(packageName, approve)
}

override fun isVerboseLogEnabled(): Boolean = real?.isVerboseLogEnabled ?: false

override fun setVerboseLogEnabled(enabled: Boolean) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,25 @@ class DaemonClient(private val serviceState: StateFlow<IManagerService?>) {
?: throw IllegalArgumentException("$packageName has no scope to read")
}


suspend fun isScopeRequestBlocked(packageName: String): Result<Boolean> = runIpc {
it.isScopeRequestBlocked(packageName)
}

suspend fun setModuleScopeRequestBlocked(
packageName: String,
block: Boolean,
): Result<Unit> = runIpc { it.setModuleScopeRequestBlocked(packageName, block) }

suspend fun isScopeRequestApproved(packageName: String): Result<Boolean> = runIpc {
it.isScopeRequestApproved(packageName)
}

suspend fun setModuleScopeRequestApproved(
packageName: String,
approve: Boolean,
): Result<Unit> = runIpc { it.setModuleScopeRequestApproved(packageName, approve) }

suspend fun isStatusNotificationEnabled(): Result<Boolean> = runIpc {
it.isStatusNotificationEnabled
}
Expand Down
Loading
Loading