mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(android): require listener access before forwarding notifications (#130173)
* fix(android): require listener access before forwarding notifications * test(android): use activity-local owner in notification settings test
This commit is contained in:
committed by
GitHub
parent
c8c6e5a990
commit
d001975d31
@@ -1079,7 +1079,7 @@ private fun NotificationSettingsScreen(
|
||||
|
||||
val notificationPermissionLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
viewModel.setNotificationForwardingEnabled(granted)
|
||||
viewModel.setNotificationForwardingEnabled(granted && DeviceNotificationListenerService.isAccessEnabled(context))
|
||||
}
|
||||
|
||||
fun setForwarding(checked: Boolean) {
|
||||
@@ -1087,12 +1087,16 @@ private fun NotificationSettingsScreen(
|
||||
viewModel.setNotificationForwardingEnabled(false)
|
||||
return
|
||||
}
|
||||
listenerEnabled = DeviceNotificationListenerService.isAccessEnabled(context)
|
||||
if (!listenerEnabled) {
|
||||
openNotificationListenerSettings(context)
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 33 && !hasPermission(context, Manifest.permission.POST_NOTIFICATIONS)) {
|
||||
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
viewModel.setNotificationForwardingEnabled(true)
|
||||
}
|
||||
listenerEnabled = DeviceNotificationListenerService.isAccessEnabled(context)
|
||||
}
|
||||
|
||||
SettingsDetailFrame(title = nativeString("Notifications"), subtitle = nativeString("Choose what reaches OpenClaw."), icon = Icons.Default.Notifications, onBack = onBack) {
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package ai.openclaw.app.ui
|
||||
|
||||
import ai.openclaw.app.MainViewModel
|
||||
import ai.openclaw.app.NodeApp
|
||||
import ai.openclaw.app.NodeRuntime
|
||||
import ai.openclaw.app.NodeRuntimeMode
|
||||
import ai.openclaw.app.SecurePrefs
|
||||
import ai.openclaw.app.node.DeviceNotificationListenerService
|
||||
import ai.openclaw.app.ui.design.ClawDesignTheme
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.ui.test.junit4.v2.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.shadow.api.Shadow
|
||||
import org.robolectric.shadows.ShadowNotificationManager
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class NotificationSettingsScreenTest {
|
||||
@get:Rule
|
||||
val composeRule = createComposeRule()
|
||||
|
||||
private lateinit var app: NodeApp
|
||||
private lateinit var prefs: SecurePrefs
|
||||
private lateinit var runtime: NodeRuntime
|
||||
private lateinit var activity: Activity
|
||||
private var originalRuntime: NodeRuntime? = null
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
app = RuntimeEnvironment.getApplication() as NodeApp
|
||||
clearPlainPreferences()
|
||||
prefs =
|
||||
SecurePrefs(
|
||||
app,
|
||||
app.getSharedPreferences("notification-settings-${UUID.randomUUID()}", Context.MODE_PRIVATE),
|
||||
)
|
||||
runtime = NodeRuntime(app, prefs, NodeRuntimeMode.ScreenshotFixture)
|
||||
originalRuntime = app.peekRuntime()
|
||||
setApplicationRuntime(runtime)
|
||||
shadowOf(app).grantPermissions(Manifest.permission.POST_NOTIFICATIONS)
|
||||
setListenerAccess(granted = false)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
setApplicationRuntime(originalRuntime)
|
||||
runtime.disconnect()
|
||||
clearPlainPreferences()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun enablingWithoutListenerAccessKeepsForwardingDisabledAndOpensSystemAccess() {
|
||||
showNotificationSettings()
|
||||
|
||||
clickForwarding()
|
||||
|
||||
assertListenerSetupRequired()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun listenerAccessIsRequestedBeforeNotificationPostingPermission() {
|
||||
shadowOf(app).denyPermissions(Manifest.permission.POST_NOTIFICATIONS)
|
||||
showNotificationSettings()
|
||||
|
||||
clickForwarding()
|
||||
|
||||
assertListenerSetupRequired()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [31])
|
||||
fun olderAndroidAlsoRequiresListenerAccessBeforeEnablingForwarding() {
|
||||
showNotificationSettings()
|
||||
|
||||
clickForwarding()
|
||||
|
||||
assertListenerSetupRequired()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun listenerRevokedAfterScreenMountCannotEnableForwarding() {
|
||||
setListenerAccess(granted = true)
|
||||
showNotificationSettings()
|
||||
setListenerAccess(granted = false)
|
||||
|
||||
clickForwarding()
|
||||
|
||||
assertListenerSetupRequired()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun grantedListenerAccessAllowsForwardingWithoutOpeningSystemAccess() {
|
||||
setListenerAccess(granted = true)
|
||||
showNotificationSettings()
|
||||
|
||||
clickForwarding()
|
||||
|
||||
assertTrue(prefs.notificationForwardingEnabled.value)
|
||||
assertNull(shadowOf(activity).nextStartedActivity)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun disablingForwardingDoesNotRequireListenerAccess() {
|
||||
prefs.setNotificationForwardingEnabled(true)
|
||||
showNotificationSettings()
|
||||
|
||||
clickForwarding()
|
||||
|
||||
assertFalse(prefs.notificationForwardingEnabled.value)
|
||||
assertNull(shadowOf(activity).nextStartedActivity)
|
||||
}
|
||||
|
||||
private fun showNotificationSettings() {
|
||||
val viewModel = MainViewModel(app, prefs, SavedStateHandle())
|
||||
composeRule.setContent {
|
||||
activity = requireNotNull(LocalActivity.current)
|
||||
ClawDesignTheme {
|
||||
SettingsDetailScreen(
|
||||
viewModel = viewModel,
|
||||
route = SettingsRoute.Notifications,
|
||||
onBack = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun clickForwarding() {
|
||||
composeRule.onNodeWithText("Forward Notifications").performClick()
|
||||
composeRule.waitForIdle()
|
||||
}
|
||||
|
||||
private fun assertListenerSetupRequired() {
|
||||
assertFalse(prefs.notificationForwardingEnabled.value)
|
||||
assertEquals(
|
||||
Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS,
|
||||
shadowOf(activity).nextStartedActivity?.action,
|
||||
)
|
||||
}
|
||||
|
||||
private fun setListenerAccess(granted: Boolean) {
|
||||
val manager = app.getSystemService(NotificationManager::class.java)
|
||||
Shadow.extract<ShadowNotificationManager>(manager).setNotificationListenerAccessGranted(
|
||||
ComponentName(app, DeviceNotificationListenerService::class.java),
|
||||
granted,
|
||||
)
|
||||
}
|
||||
|
||||
private fun setApplicationRuntime(value: NodeRuntime?) {
|
||||
NodeApp::class.java
|
||||
.getDeclaredField("runtimeInstance")
|
||||
.apply { isAccessible = true }
|
||||
.set(app, value)
|
||||
}
|
||||
|
||||
private fun clearPlainPreferences() {
|
||||
app
|
||||
.getSharedPreferences("openclaw.node", Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.clear()
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user