Say it over the app when the app is what somebody is looking at
The notification stream now has three places to land instead of two, decided in one function. Nothing at all for the session on screen, as before. A banner over the app while the app is up. Android's drawer otherwise. Never two of them for one moment: a drawer filling up behind an app that showed you each one is a drawer nobody reads. The banners queue, one per session replacing that session's own -- the rule the drawer already followed, and for the same reason. Each can be tapped, which opens the session by the same path a tapped notification takes; pushed off either side; or left alone, in which case the bar across its foot retires it. The bar and the retiring are one value rather than a bar beside a timer, so a banner cannot outlive the countdown drawn under it. They clear when the app goes away, since a claim that a session wants somebody *now* does not survive an absence -- and the drawer has the job back by then. Which of the three applies needs no flag anybody keeps level. The session on screen is registered by the one composable that draws one, and "the app is up" is the queue being collected, which happens exactly while it is. Also: tapping a model or permission button while its own menu is open now closes it. A non-focusable popup does not swallow the press that dismisses it, so the same finger was reopening what it had just closed -- measured at 3ms between the two, which is what the guard is sized against. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e0eaa4b3f8
commit
be47beb0ff
6 files changed
+296
-11
No files matched your search
@@ -20,6 +20,9 @@ import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import kotlin.concurrent.thread
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
@@ -139,6 +142,10 @@ class NotificationService : Service() {
|
||||
// them is already saying it, and a sound over the top of it would be this app announcing
|
||||
// what the screen is showing.
|
||||
if (isOnScreen(notification.sessionId)) return
|
||||
// The app is up: it says this itself, as a banner over whatever screen they are on. See
|
||||
// [forTheScreen]. Never both -- one thing happened, and a drawer filling up behind an
|
||||
// app that already showed you each one is a drawer nobody reads.
|
||||
if (handOver(notification)) return
|
||||
val manager = NotificationManagerCompat.from(this)
|
||||
// Two different noes, and both are answers rather than faults: the runtime permission
|
||||
// refused, and notifications switched off for the app in Android's own settings. Neither
|
||||
@@ -160,15 +167,7 @@ class NotificationService : Service() {
|
||||
val built =
|
||||
NotificationCompat.Builder(this, ALERT_CHANNEL)
|
||||
.setContentTitle(notification.title)
|
||||
.setContentText(
|
||||
when (notification.kind) {
|
||||
// What the reader has to do, not what the session
|
||||
// did: "awaitingInput" is the wire's word and says
|
||||
// nothing to somebody reading a lock screen.
|
||||
"awaitingInput" -> "Waiting for you"
|
||||
else -> "Finished"
|
||||
}
|
||||
)
|
||||
.setContentText(attentionLine(notification.kind))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_chat)
|
||||
.setContentIntent(open)
|
||||
.setAutoCancel(true)
|
||||
@@ -262,6 +261,23 @@ class NotificationService : Service() {
|
||||
|
||||
private fun isOnScreen(sessionId: String) = onScreen == sessionId
|
||||
|
||||
/**
|
||||
* The way a notification reaches the app instead of Android's drawer.
|
||||
*
|
||||
* Whether there is an app to reach is the subscriber count rather than a flag of its own:
|
||||
* [SessionAlerts] collects this exactly while it is on screen, so there is nothing that
|
||||
* could be left saying the app is up after it has gone. `tryEmit` neither suspends nor
|
||||
* blocks the thread reading the stream, and the buffer is there so a handful of sessions
|
||||
* finishing together all land rather than the last one winning.
|
||||
*/
|
||||
private val toApp = MutableSharedFlow<SessionNotification>(extraBufferCapacity = 8)
|
||||
|
||||
/** Everything meant for the screen rather than the drawer; see [toApp]. */
|
||||
val forTheScreen: SharedFlow<SessionNotification> = toApp.asSharedFlow()
|
||||
|
||||
private fun handOver(notification: SessionNotification) =
|
||||
toApp.subscriptionCount.value > 0 && toApp.tryEmit(notification)
|
||||
|
||||
/** Somebody is looking at [sessionId]; nothing is posted about it until they stop. */
|
||||
fun showing(context: Context, sessionId: String) {
|
||||
onScreen = sessionId
|
||||
@@ -319,6 +335,20 @@ data class SessionNotification(
|
||||
val at: Double,
|
||||
)
|
||||
|
||||
/**
|
||||
* What a notification asks of the reader, in the words they see.
|
||||
*
|
||||
* What they have to do, not what the session did: "awaitingInput" is the wire's word and says
|
||||
* nothing to somebody reading a lock screen. One function because the same fact is now shown in two
|
||||
* places -- Android's drawer and the app's own banner -- and two mappings of one word drift. The
|
||||
* banner colours the line as well, which is its own decision and stays with the drawing.
|
||||
*/
|
||||
fun attentionLine(kind: String): String =
|
||||
when (kind) {
|
||||
"awaitingInput" -> "Waiting for you"
|
||||
else -> "Finished"
|
||||
}
|
||||
|
||||
fun parseNotification(json: String): SessionNotification {
|
||||
val body = JSONObject(json)
|
||||
return SessionNotification(
|
||||
|
||||
Reference in new issue
Block a user