mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
fix(android): report disabled notification categories instead of success (#130531)
This commit is contained in:
committed by
GitHub
parent
fc51e97e60
commit
a369c7ca02
@@ -29,29 +29,14 @@ internal data class SystemNotifyRequest(
|
||||
|
||||
/** Notification posting seam used by production Android and unit tests. */
|
||||
internal interface SystemNotificationPoster {
|
||||
fun isAuthorized(): Boolean
|
||||
|
||||
fun post(request: SystemNotifyRequest)
|
||||
}
|
||||
|
||||
private class AndroidSystemNotificationPoster(
|
||||
private val appContext: Context,
|
||||
) : SystemNotificationPoster {
|
||||
/** Checks both Android 13 runtime permission and app-level notification enablement. */
|
||||
override fun isAuthorized(): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
val granted =
|
||||
ContextCompat.checkSelfPermission(appContext, Manifest.permission.POST_NOTIFICATIONS) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
if (!granted) return false
|
||||
}
|
||||
return NotificationManagerCompat.from(appContext).areNotificationsEnabled()
|
||||
}
|
||||
|
||||
/** Posts through a priority-specific channel so Android's immutable channel importance is respected. */
|
||||
override fun post(request: SystemNotifyRequest) {
|
||||
val channelId = ensureChannel(request.priority)
|
||||
val notification = buildSystemNotification(appContext, channelId, request)
|
||||
if (
|
||||
Build.VERSION.SDK_INT >= 33 &&
|
||||
ContextCompat.checkSelfPermission(appContext, Manifest.permission.POST_NOTIFICATIONS) !=
|
||||
@@ -59,6 +44,11 @@ private class AndroidSystemNotificationPoster(
|
||||
) {
|
||||
throw SecurityException("notifications permission missing")
|
||||
}
|
||||
if (!NotificationManagerCompat.from(appContext).areNotificationsEnabled()) {
|
||||
throw SecurityException("notifications disabled")
|
||||
}
|
||||
val channelId = ensureChannel(request.priority)
|
||||
val notification = buildSystemNotification(appContext, channelId, request)
|
||||
NotificationManagerCompat.from(appContext).notify((System.currentTimeMillis() and 0x7FFFFFFF).toInt(), notification)
|
||||
}
|
||||
|
||||
@@ -82,6 +72,10 @@ private class AndroidSystemNotificationPoster(
|
||||
val channelId = "$NOTIFICATION_CHANNEL_BASE_ID.$suffix"
|
||||
val manager = appContext.getSystemService(NotificationManager::class.java)
|
||||
val existing = manager.getNotificationChannel(channelId)
|
||||
// notify() silently drops blocked channels; report the user's existing choice before posting.
|
||||
if (existing?.importance == NotificationManager.IMPORTANCE_NONE) {
|
||||
throw SecurityException("notification channel disabled")
|
||||
}
|
||||
if (existing == null) {
|
||||
manager.createNotificationChannel(NotificationChannel(channelId, name, importance))
|
||||
}
|
||||
@@ -138,19 +132,13 @@ class SystemHandler private constructor(
|
||||
message = "INVALID_REQUEST: empty notification",
|
||||
)
|
||||
}
|
||||
if (!poster.isAuthorized()) {
|
||||
return GatewaySession.InvokeResult.error(
|
||||
code = "NOT_AUTHORIZED",
|
||||
message = "NOT_AUTHORIZED: notifications",
|
||||
)
|
||||
}
|
||||
return try {
|
||||
poster.post(params)
|
||||
GatewaySession.InvokeResult.ok(null)
|
||||
} catch (_: SecurityException) {
|
||||
GatewaySession.InvokeResult.error(
|
||||
code = "NOT_AUTHORIZED",
|
||||
message = "NOT_AUTHORIZED: notifications",
|
||||
message = "NOT_AUTHORIZED: enable OpenClaw notifications and the selected priority in Android Settings",
|
||||
)
|
||||
} catch (err: Throwable) {
|
||||
GatewaySession.InvokeResult.error(
|
||||
|
||||
@@ -386,8 +386,6 @@ private class InvokeDispatcherFakeNotificationsStateProvider : NotificationsStat
|
||||
}
|
||||
|
||||
private class InvokeDispatcherFakeSystemNotificationPoster : SystemNotificationPoster {
|
||||
override fun isAuthorized(): Boolean = true
|
||||
|
||||
override fun post(request: SystemNotifyRequest) = Unit
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package ai.openclaw.app.node
|
||||
|
||||
import ai.openclaw.app.MainActivity
|
||||
import android.Manifest
|
||||
import android.app.Application
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -19,17 +23,30 @@ import org.robolectric.annotation.Config
|
||||
class SystemHandlerTest {
|
||||
@Test
|
||||
fun handleSystemNotify_rejectsUnauthorized() {
|
||||
val handler = SystemHandler.forTesting(poster = FakePoster(authorized = false))
|
||||
val context: Application = RuntimeEnvironment.getApplication()
|
||||
val manager = context.getSystemService(NotificationManager::class.java)
|
||||
val handler = SystemHandler(context)
|
||||
|
||||
val result = handler.handleSystemNotify("""{"title":"OpenClaw","body":"hi"}""")
|
||||
for (permissionGranted in listOf(false, true)) {
|
||||
if (permissionGranted) {
|
||||
Shadows.shadowOf(context).grantPermissions(Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
Shadows.shadowOf(context).denyPermissions(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
Shadows.shadowOf(manager).setNotificationsEnabled(!permissionGranted)
|
||||
|
||||
assertFalse(result.ok)
|
||||
assertEquals("NOT_AUTHORIZED", result.error?.code)
|
||||
val result = handler.handleSystemNotify("""{"title":"OpenClaw","body":"hi"}""")
|
||||
|
||||
assertFalse(result.ok)
|
||||
assertEquals("NOT_AUTHORIZED", result.error?.code)
|
||||
assertTrue(manager.notificationChannels.isEmpty())
|
||||
assertTrue(manager.activeNotifications.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSystemNotify_rejectsEmptyNotification() {
|
||||
val handler = SystemHandler.forTesting(poster = FakePoster(authorized = true))
|
||||
val handler = SystemHandler.forTesting(poster = FakePoster())
|
||||
|
||||
val result = handler.handleSystemNotify("""{"title":" ","body":" "}""")
|
||||
|
||||
@@ -39,7 +56,7 @@ class SystemHandlerTest {
|
||||
|
||||
@Test
|
||||
fun handleSystemNotify_rejectsInvalidRequestObject() {
|
||||
val handler = SystemHandler.forTesting(poster = FakePoster(authorized = true))
|
||||
val handler = SystemHandler.forTesting(poster = FakePoster())
|
||||
|
||||
val result = handler.handleSystemNotify("""{"title":"OpenClaw"}""")
|
||||
|
||||
@@ -49,7 +66,7 @@ class SystemHandlerTest {
|
||||
|
||||
@Test
|
||||
fun handleSystemNotify_postsNotification() {
|
||||
val poster = FakePoster(authorized = true)
|
||||
val poster = FakePoster()
|
||||
val handler = SystemHandler.forTesting(poster = poster)
|
||||
|
||||
val result = handler.handleSystemNotify("""{"title":"OpenClaw","body":"done","priority":"active"}""")
|
||||
@@ -58,9 +75,50 @@ class SystemHandlerTest {
|
||||
assertEquals(1, poster.posts)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSystemNotify_rejectsBlockedSelectedChannels() {
|
||||
val context: Application = RuntimeEnvironment.getApplication()
|
||||
Shadows.shadowOf(context).grantPermissions(Manifest.permission.POST_NOTIFICATIONS)
|
||||
val manager = context.getSystemService(NotificationManager::class.java)
|
||||
val handler = SystemHandler(context)
|
||||
assertTrue(manager.areNotificationsEnabled())
|
||||
|
||||
val priorities = listOf(null to "active", "active" to "active", "passive" to "passive", "timeSensitive" to "timesensitive")
|
||||
for ((priority, suffix) in priorities) {
|
||||
val channelId = "openclaw.system.notify.$suffix"
|
||||
manager.createNotificationChannel(NotificationChannel(channelId, "Blocked", NotificationManager.IMPORTANCE_NONE))
|
||||
val priorityField = priority?.let { ",\"priority\":\"$it\"" }.orEmpty()
|
||||
|
||||
val result = handler.handleSystemNotify("""{"title":"OpenClaw","body":"blocked"$priorityField}""")
|
||||
|
||||
assertFalse("priority=$priority must not report a blocked post as successful", result.ok)
|
||||
assertEquals("NOT_AUTHORIZED", result.error?.code)
|
||||
assertEquals(NotificationManager.IMPORTANCE_NONE, manager.getNotificationChannel(channelId).importance)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSystemNotify_postsAllowedChannelWhenAnotherPriorityIsBlocked() {
|
||||
val context: Application = RuntimeEnvironment.getApplication()
|
||||
Shadows.shadowOf(context).grantPermissions(Manifest.permission.POST_NOTIFICATIONS)
|
||||
val manager = context.getSystemService(NotificationManager::class.java)
|
||||
manager.createNotificationChannel(
|
||||
NotificationChannel("openclaw.system.notify.active", "Blocked", NotificationManager.IMPORTANCE_NONE),
|
||||
)
|
||||
val handler = SystemHandler(context)
|
||||
|
||||
val result = handler.handleSystemNotify("""{"title":"OpenClaw","body":"allowed","priority":"passive"}""")
|
||||
|
||||
assertTrue(result.ok)
|
||||
assertEquals(NotificationManager.IMPORTANCE_LOW, manager.getNotificationChannel("openclaw.system.notify.passive").importance)
|
||||
val notification = manager.activeNotifications.single().notification
|
||||
assertEquals("openclaw.system.notify.passive", notification.channelId)
|
||||
assertEquals("allowed", notification.extras.getCharSequence("android.text"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSystemNotify_trimsAndPassesOptionalFields() {
|
||||
val poster = FakePoster(authorized = true)
|
||||
val poster = FakePoster()
|
||||
val handler = SystemHandler.forTesting(poster = poster)
|
||||
|
||||
val result =
|
||||
@@ -97,7 +155,7 @@ class SystemHandlerTest {
|
||||
|
||||
@Test
|
||||
fun handleSystemNotify_returnsUnauthorizedWhenPostFailsPermission() {
|
||||
val handler = SystemHandler.forTesting(poster = ThrowingPoster(authorized = true, error = SecurityException("denied")))
|
||||
val handler = SystemHandler.forTesting(poster = ThrowingPoster(error = SecurityException("denied")))
|
||||
|
||||
val result = handler.handleSystemNotify("""{"title":"OpenClaw","body":"done"}""")
|
||||
|
||||
@@ -107,7 +165,7 @@ class SystemHandlerTest {
|
||||
|
||||
@Test
|
||||
fun handleSystemNotify_returnsUnavailableWhenPostFailsUnexpectedly() {
|
||||
val handler = SystemHandler.forTesting(poster = ThrowingPoster(authorized = true, error = IllegalStateException("boom")))
|
||||
val handler = SystemHandler.forTesting(poster = ThrowingPoster(error = IllegalStateException("boom")))
|
||||
|
||||
val result = handler.handleSystemNotify("""{"title":"OpenClaw","body":"done"}""")
|
||||
|
||||
@@ -117,16 +175,12 @@ class SystemHandlerTest {
|
||||
}
|
||||
}
|
||||
|
||||
private class FakePoster(
|
||||
private val authorized: Boolean,
|
||||
) : SystemNotificationPoster {
|
||||
private class FakePoster : SystemNotificationPoster {
|
||||
var posts: Int = 0
|
||||
private set
|
||||
var lastRequest: SystemNotifyRequest? = null
|
||||
private set
|
||||
|
||||
override fun isAuthorized(): Boolean = authorized
|
||||
|
||||
override fun post(request: SystemNotifyRequest) {
|
||||
posts += 1
|
||||
lastRequest = request
|
||||
@@ -134,10 +188,7 @@ private class FakePoster(
|
||||
}
|
||||
|
||||
private class ThrowingPoster(
|
||||
private val authorized: Boolean,
|
||||
private val error: Throwable,
|
||||
) : SystemNotificationPoster {
|
||||
override fun isAuthorized(): Boolean = authorized
|
||||
|
||||
override fun post(request: SystemNotifyRequest): Unit = throw error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user