mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
Merge pull request #861 from permissionlesstech/codex/delay-peer-availability-notification
Delay nearby peer notification to collect accurate count
This commit is contained in:
commit
094657efa0
@ -101,7 +101,16 @@ class MeshForegroundService : Service() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
peerAvailabilityNotifier = PeerAvailabilityNotifier(applicationContext)
|
||||
peerAvailabilityNotifier = PeerAvailabilityNotifier(
|
||||
context = applicationContext,
|
||||
scope = scope,
|
||||
isAppCurrentlyInBackground = {
|
||||
!ProcessLifecycleOwner.get()
|
||||
.lifecycle
|
||||
.currentState
|
||||
.isAtLeast(Lifecycle.State.STARTED)
|
||||
}
|
||||
)
|
||||
createChannel()
|
||||
|
||||
// Ensure mesh service exists in holder (create if needed)
|
||||
|
||||
@ -16,6 +16,10 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import com.bitchat.android.MainActivity
|
||||
import com.bitchat.android.R
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal enum class PeerAvailabilityAction {
|
||||
NONE,
|
||||
@ -151,9 +155,12 @@ private class AndroidPeerAvailabilityTextProvider(
|
||||
|
||||
/**
|
||||
* Owns the user-visible "bitchatters nearby" notification independently of the UI delegate.
|
||||
* The first eligible peer starts a fixed aggregation window; arrivals within that window update
|
||||
* the count without postponing the alert indefinitely.
|
||||
*/
|
||||
internal class PeerAvailabilityNotifier(
|
||||
private val context: Context,
|
||||
private val scope: CoroutineScope,
|
||||
private val notificationManager: NotificationManagerCompat =
|
||||
NotificationManagerCompat.from(context),
|
||||
private val tracker: PeerAvailabilityTracker = PeerAvailabilityTracker(
|
||||
@ -161,6 +168,8 @@ internal class PeerAvailabilityNotifier(
|
||||
),
|
||||
private val textProvider: PeerAvailabilityTextProvider =
|
||||
AndroidPeerAvailabilityTextProvider(context),
|
||||
private val isAppCurrentlyInBackground: () -> Boolean,
|
||||
private val aggregationWindowMs: Long = AGGREGATION_WINDOW_MS,
|
||||
private val canPostNotifications: () -> Boolean = {
|
||||
notificationManager.areNotificationsEnabled() &&
|
||||
(
|
||||
@ -175,25 +184,47 @@ internal class PeerAvailabilityNotifier(
|
||||
companion object {
|
||||
internal const val CHANNEL_ID = "bitchat_peer_availability_notifications"
|
||||
internal const val NOTIFICATION_ID = 997
|
||||
internal const val AGGREGATION_WINDOW_MS = 10_000L
|
||||
private const val TAG = "PeerAvailability"
|
||||
}
|
||||
|
||||
private var latestPeerCount = 0
|
||||
private var pendingNotificationJob: Job? = null
|
||||
|
||||
init {
|
||||
require(aggregationWindowMs >= 0) { "aggregationWindowMs must not be negative" }
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
fun onPeerCountChanged(peerCount: Int, isAppInBackground: Boolean) {
|
||||
latestPeerCount = peerCount
|
||||
when (tracker.update(peerCount, isAppInBackground)) {
|
||||
PeerAvailabilityAction.NONE -> Unit
|
||||
PeerAvailabilityAction.CLEAR -> clear()
|
||||
PeerAvailabilityAction.SHOW -> showNotification(peerCount)
|
||||
PeerAvailabilityAction.SHOW -> scheduleNotification()
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
pendingNotificationJob?.cancel()
|
||||
pendingNotificationJob = null
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
private fun scheduleNotification() {
|
||||
if (pendingNotificationJob?.isActive == true) return
|
||||
|
||||
pendingNotificationJob = scope.launch {
|
||||
delay(aggregationWindowMs)
|
||||
pendingNotificationJob = null
|
||||
|
||||
val peerCount = latestPeerCount
|
||||
if (peerCount > 0 && isAppCurrentlyInBackground()) {
|
||||
showNotification(peerCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
|
||||
@ -4,6 +4,11 @@ import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
@ -121,6 +126,7 @@ class PeerAvailabilityTrackerTest {
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [32])
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class PeerAvailabilityNotifierTest {
|
||||
private lateinit var context: Context
|
||||
private lateinit var systemNotificationManager: NotificationManager
|
||||
@ -138,18 +144,25 @@ class PeerAvailabilityNotifierTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `background availability posts on dedicated channel`() {
|
||||
fun `background availability waits ten seconds before posting on dedicated channel`() = runTest {
|
||||
val textProvider = testTextProvider()
|
||||
val notifier = PeerAvailabilityNotifier(
|
||||
context = context,
|
||||
tracker = tracker(),
|
||||
textProvider = textProvider,
|
||||
canPostNotifications = { true }
|
||||
val notifier = createNotifier(
|
||||
scope = this,
|
||||
textProvider = textProvider
|
||||
)
|
||||
|
||||
notifier.onPeerCountChanged(0, isAppInBackground = true)
|
||||
notifier.onPeerCountChanged(2, isAppInBackground = true)
|
||||
|
||||
advanceTimeBy(PeerAvailabilityNotifier.AGGREGATION_WINDOW_MS - 1)
|
||||
assertNull(
|
||||
shadowOf(systemNotificationManager).getNotification(
|
||||
PeerAvailabilityNotifier.NOTIFICATION_ID
|
||||
)
|
||||
)
|
||||
|
||||
advanceTimeBy(1)
|
||||
runCurrent()
|
||||
val notification =
|
||||
shadowOf(systemNotificationManager).getNotification(
|
||||
PeerAvailabilityNotifier.NOTIFICATION_ID
|
||||
@ -171,16 +184,63 @@ class PeerAvailabilityNotifierTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returning to zero cancels availability notification`() {
|
||||
val notifier = PeerAvailabilityNotifier(
|
||||
context = context,
|
||||
tracker = tracker(),
|
||||
textProvider = testTextProvider(),
|
||||
canPostNotifications = { true }
|
||||
fun `later peer arrivals are included without restarting aggregation window`() = runTest {
|
||||
val textProvider = testTextProvider()
|
||||
val notifier = createNotifier(
|
||||
scope = this,
|
||||
textProvider = textProvider
|
||||
)
|
||||
|
||||
notifier.onPeerCountChanged(1, isAppInBackground = true)
|
||||
advanceTimeBy(6_000L)
|
||||
notifier.onPeerCountChanged(3, isAppInBackground = true)
|
||||
advanceTimeBy(4_000L)
|
||||
runCurrent()
|
||||
|
||||
val notification =
|
||||
shadowOf(systemNotificationManager).getNotification(
|
||||
PeerAvailabilityNotifier.NOTIFICATION_ID
|
||||
)
|
||||
assertNotNull(notification)
|
||||
assertEquals(
|
||||
textProvider.body(3),
|
||||
notification.extras.getString("android.text")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `opening app during aggregation suppresses notification`() = runTest {
|
||||
var isAppInBackground = true
|
||||
val history = InMemoryAlertHistory()
|
||||
val notifier = createNotifier(
|
||||
scope = this,
|
||||
availabilityTracker = PeerAvailabilityTracker(history),
|
||||
isAppCurrentlyInBackground = { isAppInBackground }
|
||||
)
|
||||
|
||||
notifier.onPeerCountChanged(1, isAppInBackground = true)
|
||||
advanceTimeBy(PeerAvailabilityNotifier.AGGREGATION_WINDOW_MS / 2)
|
||||
isAppInBackground = false
|
||||
advanceTimeBy(PeerAvailabilityNotifier.AGGREGATION_WINDOW_MS / 2)
|
||||
runCurrent()
|
||||
|
||||
assertNull(
|
||||
shadowOf(systemNotificationManager).getNotification(
|
||||
PeerAvailabilityNotifier.NOTIFICATION_ID
|
||||
)
|
||||
)
|
||||
assertNull(history.lastAlertAtMillis)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returning to zero cancels pending availability notification`() = runTest {
|
||||
val notifier = createNotifier(scope = this)
|
||||
|
||||
notifier.onPeerCountChanged(1, isAppInBackground = true)
|
||||
advanceTimeBy(PeerAvailabilityNotifier.AGGREGATION_WINDOW_MS / 2)
|
||||
notifier.onPeerCountChanged(0, isAppInBackground = true)
|
||||
advanceTimeBy(PeerAvailabilityNotifier.AGGREGATION_WINDOW_MS)
|
||||
runCurrent()
|
||||
|
||||
assertNull(
|
||||
shadowOf(systemNotificationManager).getNotification(
|
||||
@ -190,16 +250,13 @@ class PeerAvailabilityNotifierTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `explicit clear cancels availability notification`() {
|
||||
val notifier = PeerAvailabilityNotifier(
|
||||
context = context,
|
||||
tracker = tracker(),
|
||||
textProvider = testTextProvider(),
|
||||
canPostNotifications = { true }
|
||||
)
|
||||
fun `explicit clear cancels pending availability notification`() = runTest {
|
||||
val notifier = createNotifier(scope = this)
|
||||
|
||||
notifier.onPeerCountChanged(1, isAppInBackground = true)
|
||||
notifier.clear()
|
||||
advanceTimeBy(PeerAvailabilityNotifier.AGGREGATION_WINDOW_MS)
|
||||
runCurrent()
|
||||
|
||||
assertNull(
|
||||
shadowOf(systemNotificationManager).getNotification(
|
||||
@ -209,16 +266,17 @@ class PeerAvailabilityNotifierTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disabled notifications do not post`() {
|
||||
fun `disabled notifications do not post`() = runTest {
|
||||
val history = InMemoryAlertHistory()
|
||||
val notifier = PeerAvailabilityNotifier(
|
||||
context = context,
|
||||
tracker = PeerAvailabilityTracker(history),
|
||||
textProvider = testTextProvider(),
|
||||
val notifier = createNotifier(
|
||||
scope = this,
|
||||
availabilityTracker = PeerAvailabilityTracker(history),
|
||||
canPostNotifications = { false }
|
||||
)
|
||||
|
||||
notifier.onPeerCountChanged(1, isAppInBackground = true)
|
||||
advanceTimeBy(PeerAvailabilityNotifier.AGGREGATION_WINDOW_MS)
|
||||
runCurrent()
|
||||
|
||||
assertNull(
|
||||
shadowOf(systemNotificationManager).getNotification(
|
||||
@ -243,6 +301,23 @@ class PeerAvailabilityNotifierTest {
|
||||
return PeerAvailabilityTracker(InMemoryAlertHistory())
|
||||
}
|
||||
|
||||
private fun createNotifier(
|
||||
scope: CoroutineScope,
|
||||
availabilityTracker: PeerAvailabilityTracker = tracker(),
|
||||
textProvider: PeerAvailabilityTextProvider = testTextProvider(),
|
||||
isAppCurrentlyInBackground: () -> Boolean = { true },
|
||||
canPostNotifications: () -> Boolean = { true }
|
||||
): PeerAvailabilityNotifier {
|
||||
return PeerAvailabilityNotifier(
|
||||
context = context,
|
||||
scope = scope,
|
||||
tracker = availabilityTracker,
|
||||
textProvider = textProvider,
|
||||
isAppCurrentlyInBackground = isAppCurrentlyInBackground,
|
||||
canPostNotifications = canPostNotifications
|
||||
)
|
||||
}
|
||||
|
||||
private fun testTextProvider(): PeerAvailabilityTextProvider {
|
||||
return object : PeerAvailabilityTextProvider {
|
||||
override fun title(): String = "Bitchatters nearby"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user