mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
Merge pull request #816 from permissionlesstech/codex/fix-active-peer-notifications
Restore background peer availability alerts
This commit is contained in:
commit
5b79da2db6
@ -68,6 +68,7 @@ object TestHookDriver {
|
||||
"file_cancel" -> fileCancel(context, intent.requiredString("transfer_id"))
|
||||
"raw_send" -> rawSend(context, intent)
|
||||
"ble" -> setBle(intent.getBooleanExtra("enabled", true))
|
||||
"inject_peers" -> injectPeers(intent.getStringExtra("peers"))
|
||||
"state" -> state(context)
|
||||
"clear_results" -> clearResults(context)
|
||||
else -> err(cmd, "unknown command: $cmd")
|
||||
@ -132,6 +133,21 @@ object TestHookDriver {
|
||||
return ok("peers").put("peers", peerInfosJson(mesh, AppStateStore.peers.value))
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug-only state injection for testing peer-list consumers such as notifications.
|
||||
* A blank or missing comma-separated value restores the empty state.
|
||||
*/
|
||||
private fun injectPeers(commaSeparatedPeers: String?): JSONObject {
|
||||
val peers = commaSeparatedPeers
|
||||
.orEmpty()
|
||||
.split(',')
|
||||
.map(String::trim)
|
||||
.filter(String::isNotEmpty)
|
||||
.distinct()
|
||||
AppStateStore.setPeers(peers)
|
||||
return ok("inject_peers").put("peers", JSONArray(peers))
|
||||
}
|
||||
|
||||
private suspend fun connect(peerID: String, intent: Intent): JSONObject {
|
||||
val timeoutMs = intent.getLongExtra("timeout_ms", DEFAULT_CONNECT_TIMEOUT_MS)
|
||||
val ble = MeshServiceHolder.meshService ?: return err("connect", "BLE service not running")
|
||||
|
||||
@ -127,16 +127,6 @@
|
||||
tools:ignore="DataExtractionRules">
|
||||
</service>
|
||||
|
||||
<!-- Listen for in-app broadcast when POST_NOTIFICATIONS is granted -->
|
||||
<receiver
|
||||
android:name=".service.NotificationPermissionChangedReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.bitchat.android.action.NOTIFICATION_PERMISSION_GRANTED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".service.ConversationNotificationReceiver"
|
||||
android:enabled="true"
|
||||
|
||||
@ -428,6 +428,8 @@ class MainActivity : OrientationAwareActivity() {
|
||||
|
||||
if (permissionManager.isFirstTimeLaunch()) {
|
||||
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
|
||||
} else if (permissionManager.getUnrequestedOptionalPermissions().isNotEmpty()) {
|
||||
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
|
||||
} else if (permissionManager.areRequiredPermissionsGranted()) {
|
||||
if (permissionManager.needsBackgroundLocationPermission() &&
|
||||
!permissionManager.isBackgroundLocationGranted() &&
|
||||
|
||||
@ -115,8 +115,7 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
||||
// Service-level notification manager for background (no-UI) DMs
|
||||
private val serviceNotificationManager = com.bitchat.android.ui.NotificationManager(
|
||||
context.applicationContext,
|
||||
androidx.core.app.NotificationManagerCompat.from(context.applicationContext),
|
||||
com.bitchat.android.util.NotificationIntervalManager()
|
||||
androidx.core.app.NotificationManagerCompat.from(context.applicationContext)
|
||||
)
|
||||
|
||||
// Service state management
|
||||
|
||||
@ -85,9 +85,7 @@ class OnboardingCoordinator(
|
||||
val missingRequired = permissionManager.getMissingPermissions()
|
||||
|
||||
// Optional permissions (ask, but do not block if denied)
|
||||
val optionalToRequest = permissionManager
|
||||
.getOptionalPermissions()
|
||||
.filter { !permissionManager.isPermissionGranted(it) }
|
||||
val optionalToRequest = permissionManager.getUnrequestedOptionalPermissions()
|
||||
|
||||
val missingPermissions = (missingRequired + optionalToRequest).distinct()
|
||||
|
||||
@ -101,6 +99,7 @@ class OnboardingCoordinator(
|
||||
}
|
||||
|
||||
Log.d(TAG, "Requesting ${missingPermissions.size} permissions")
|
||||
permissionManager.markOptionalPermissionsRequested(optionalToRequest)
|
||||
permissionLauncher?.launch(missingPermissions.toTypedArray())
|
||||
}
|
||||
|
||||
@ -115,7 +114,10 @@ class OnboardingCoordinator(
|
||||
|
||||
val allGranted = permissions.values.all { it }
|
||||
val criticalPermissions = getCriticalPermissions()
|
||||
val criticalGranted = criticalPermissions.all { permissions[it] == true }
|
||||
// The launcher result only contains permissions requested in this round. Returning
|
||||
// users may be asked for POST_NOTIFICATIONS alone, so re-check required permissions
|
||||
// against package state instead of treating absent result-map entries as denials.
|
||||
val criticalGranted = criticalPermissions.all(permissionManager::isPermissionGranted)
|
||||
|
||||
when {
|
||||
criticalGranted -> {
|
||||
|
||||
@ -19,6 +19,8 @@ class PermissionManager(private val context: Context) {
|
||||
private const val TAG = "PermissionManager"
|
||||
private const val PREFS_NAME = "bitchat_permissions"
|
||||
private const val KEY_FIRST_TIME_COMPLETE = "first_time_onboarding_complete"
|
||||
private const val KEY_OPTIONAL_PERMISSION_REQUESTED_PREFIX =
|
||||
"optional_permission_requested_"
|
||||
}
|
||||
|
||||
private val sharedPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
@ -149,6 +151,32 @@ class PermissionManager(private val context: Context) {
|
||||
return optional
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional permissions are prompted once. A denial must not trap returning users in
|
||||
* onboarding, while users upgrading to a notification-permission Android version
|
||||
* should still receive one contextual request.
|
||||
*/
|
||||
fun getUnrequestedOptionalPermissions(): List<String> {
|
||||
return getOptionalPermissions().filter { permission ->
|
||||
!isPermissionGranted(permission) &&
|
||||
!sharedPrefs.getBoolean(optionalPermissionRequestKey(permission), false)
|
||||
}
|
||||
}
|
||||
|
||||
fun markOptionalPermissionsRequested(permissions: Collection<String>) {
|
||||
if (permissions.isEmpty()) return
|
||||
|
||||
sharedPrefs.edit().apply {
|
||||
permissions.forEach { permission ->
|
||||
putBoolean(optionalPermissionRequestKey(permission), true)
|
||||
}
|
||||
}.apply()
|
||||
}
|
||||
|
||||
private fun optionalPermissionRequestKey(permission: String): String {
|
||||
return KEY_OPTIONAL_PERMISSION_REQUESTED_PREFIX + permission
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific permission is granted
|
||||
*/
|
||||
|
||||
@ -13,6 +13,8 @@ import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import com.bitchat.android.MainActivity
|
||||
import com.bitchat.android.R
|
||||
import com.bitchat.android.mesh.BluetoothMeshService
|
||||
@ -33,7 +35,6 @@ class MeshForegroundService : Service() {
|
||||
const val ACTION_STOP = "com.bitchat.android.service.STOP"
|
||||
const val ACTION_QUIT = "com.bitchat.android.service.QUIT"
|
||||
const val ACTION_UPDATE_NOTIFICATION = "com.bitchat.android.service.UPDATE_NOTIFICATION"
|
||||
const val ACTION_NOTIFICATION_PERMISSION_GRANTED = "com.bitchat.android.action.NOTIFICATION_PERMISSION_GRANTED"
|
||||
|
||||
fun start(context: Context) {
|
||||
val intent = Intent(context, MeshForegroundService::class.java).apply { action = ACTION_START }
|
||||
@ -59,22 +60,6 @@ class MeshForegroundService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to be invoked right after POST_NOTIFICATIONS is granted to try
|
||||
* promoting/starting the foreground service immediately without polling.
|
||||
*/
|
||||
fun onNotificationPermissionGranted(context: Context) {
|
||||
// If background is enabled and permission now granted, start/promo service
|
||||
if (!shouldStartAsForeground(context)) return
|
||||
|
||||
val intent = Intent(context, MeshForegroundService::class.java).apply { action = ACTION_UPDATE_NOTIFICATION }
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
val intent = Intent(context, MeshForegroundService::class.java).apply { action = ACTION_STOP }
|
||||
context.startService(intent)
|
||||
@ -82,8 +67,7 @@ class MeshForegroundService : Service() {
|
||||
|
||||
private fun shouldStartAsForeground(context: Context): Boolean {
|
||||
return MeshServicePreferences.isBackgroundEnabled(true) &&
|
||||
hasBluetoothPermissionsStatic(context) &&
|
||||
hasNotificationPermissionStatic(context)
|
||||
hasBluetoothPermissionsStatic(context)
|
||||
}
|
||||
|
||||
private fun hasBluetoothPermissionsStatic(ctx: Context): Boolean {
|
||||
@ -98,14 +82,10 @@ class MeshForegroundService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasNotificationPermissionStatic(ctx: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= 33) {
|
||||
androidx.core.content.ContextCompat.checkSelfPermission(ctx, android.Manifest.permission.POST_NOTIFICATIONS) == android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
} else true
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var notificationManager: NotificationManagerCompat
|
||||
private lateinit var peerAvailabilityNotifier: PeerAvailabilityNotifier
|
||||
private var updateJob: Job? = null
|
||||
private val meshService: BluetoothMeshService?
|
||||
get() = MeshServiceHolder.meshService
|
||||
@ -121,6 +101,7 @@ class MeshForegroundService : Service() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
peerAvailabilityNotifier = PeerAvailabilityNotifier(applicationContext)
|
||||
createChannel()
|
||||
|
||||
// Ensure mesh service exists in holder (create if needed)
|
||||
@ -139,7 +120,14 @@ class MeshForegroundService : Service() {
|
||||
com.bitchat.android.services.AppStateStore.peers
|
||||
.map { peers -> peers.distinct().size }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
.collect { peerCount ->
|
||||
peerAvailabilityNotifier.onPeerCountChanged(
|
||||
peerCount = peerCount,
|
||||
isAppInBackground = !ProcessLifecycleOwner.get()
|
||||
.lifecycle
|
||||
.currentState
|
||||
.isAtLeast(Lifecycle.State.STARTED)
|
||||
)
|
||||
if (isInForeground) updateNotification(force = false)
|
||||
}
|
||||
}
|
||||
@ -156,11 +144,13 @@ class MeshForegroundService : Service() {
|
||||
when (intent?.action) {
|
||||
ACTION_STOP -> {
|
||||
// Stop FGS and mesh cleanly
|
||||
updateJob?.cancel()
|
||||
updateJob = null
|
||||
try { com.bitchat.android.services.MessageRouter.tryGetInstance()?.stopOutboxScheduler() } catch (_: Exception) { }
|
||||
try { unifiedMeshService?.stopServices() ?: meshService?.stopServices() } catch (_: Exception) { }
|
||||
try { MeshServiceHolder.clear() } catch (_: Exception) { }
|
||||
try { stopForeground(true) } catch (_: Exception) { }
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
clearMeshNotifications()
|
||||
isInForeground = false
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
@ -170,7 +160,7 @@ class MeshForegroundService : Service() {
|
||||
updateJob?.cancel()
|
||||
updateJob = null
|
||||
try { stopForeground(true) } catch (_: Exception) { }
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
clearMeshNotifications()
|
||||
isInForeground = false
|
||||
// Fully stop all background activity, stop Tor (without changing setting), then kill the app
|
||||
AppShutdownCoordinator.requestFullShutdownAndKill(
|
||||
@ -203,7 +193,9 @@ class MeshForegroundService : Service() {
|
||||
// Ensure mesh is running (only after permissions are granted)
|
||||
ensureMeshStarted()
|
||||
|
||||
// Promote exactly once when eligible, otherwise stay background (or stop)
|
||||
// Promote exactly once when eligible, otherwise stay background (or stop).
|
||||
// POST_NOTIFICATIONS is intentionally not an eligibility requirement: Android 13+
|
||||
// still allows foreground services and exposes them in the system task manager.
|
||||
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions() && !isInForeground) {
|
||||
val count = getUnifiedActivePeerCount()
|
||||
val notification = buildNotification(count)
|
||||
@ -234,30 +226,35 @@ class MeshForegroundService : Service() {
|
||||
|
||||
private fun updateNotification(force: Boolean) {
|
||||
if (isShuttingDown) {
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
clearMeshNotifications()
|
||||
return
|
||||
}
|
||||
val count = getUnifiedActivePeerCount()
|
||||
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions()) {
|
||||
if (lastNotifiedPeerCount != count) {
|
||||
notificationManager.notify(NOTIFICATION_ID, buildNotification(count))
|
||||
startForegroundCompat(buildNotification(count))
|
||||
lastNotifiedPeerCount = count
|
||||
}
|
||||
} else if (force) {
|
||||
// If disabled and forced, make sure to remove any prior foreground state
|
||||
try { stopForeground(false) } catch (_: Exception) { }
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
clearMeshNotifications()
|
||||
isInForeground = false
|
||||
lastNotifiedPeerCount = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearMeshNotifications() {
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
peerAvailabilityNotifier.clear()
|
||||
}
|
||||
|
||||
private fun hasAllRequiredPermissions(): Boolean {
|
||||
// For starting FGS with connectedDevice|dataSync, we need:
|
||||
// - Foreground service permissions (declared in manifest)
|
||||
// - One of the device-related permissions (we request BL perms at runtime)
|
||||
// - On Android 13+, POST_NOTIFICATIONS to actually show notification
|
||||
return hasBluetoothPermissions() && hasNotificationPermission()
|
||||
// POST_NOTIFICATIONS controls notification-drawer visibility, not FGS eligibility.
|
||||
return hasBluetoothPermissions()
|
||||
}
|
||||
|
||||
private fun getUnifiedActivePeerCount(): Int {
|
||||
@ -281,12 +278,6 @@ class MeshForegroundService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasNotificationPermission(): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= 33) {
|
||||
androidx.core.content.ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) == android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
} else true
|
||||
}
|
||||
|
||||
private fun buildNotification(activePeers: Int): Notification {
|
||||
val openIntent = Intent(this, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
|
||||
@ -0,0 +1,249 @@
|
||||
package com.bitchat.android.service
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import com.bitchat.android.MainActivity
|
||||
import com.bitchat.android.R
|
||||
|
||||
internal enum class PeerAvailabilityAction {
|
||||
NONE,
|
||||
SHOW,
|
||||
CLEAR
|
||||
}
|
||||
|
||||
internal interface PeerAvailabilityAlertHistory {
|
||||
var lastAlertAtMillis: Long?
|
||||
}
|
||||
|
||||
internal class SharedPreferencesPeerAvailabilityAlertHistory(
|
||||
context: Context
|
||||
) : PeerAvailabilityAlertHistory {
|
||||
companion object {
|
||||
internal const val PREFERENCES_NAME = "peer_availability_notifications"
|
||||
private const val KEY_LAST_ALERT_AT_MILLIS = "last_alert_at_millis"
|
||||
}
|
||||
|
||||
private val preferences =
|
||||
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
override var lastAlertAtMillis: Long?
|
||||
get() = if (preferences.contains(KEY_LAST_ALERT_AT_MILLIS)) {
|
||||
preferences.getLong(KEY_LAST_ALERT_AT_MILLIS, 0L)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
set(value) {
|
||||
preferences.edit {
|
||||
if (value == null) {
|
||||
remove(KEY_LAST_ALERT_AT_MILLIS)
|
||||
} else {
|
||||
putLong(KEY_LAST_ALERT_AT_MILLIS, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks mesh availability epochs with two anti-flapping gates:
|
||||
* - no more than one alert per persisted cooldown window;
|
||||
* - after an alert, the mesh must remain empty before another epoch can re-arm.
|
||||
*/
|
||||
internal class PeerAvailabilityTracker(
|
||||
private val alertHistory: PeerAvailabilityAlertHistory,
|
||||
private val nowMillis: () -> Long = System::currentTimeMillis,
|
||||
private val alertCooldownMs: Long = ALERT_COOLDOWN_MS,
|
||||
private val emptyRearmDelayMs: Long = EMPTY_REARM_DELAY_MS
|
||||
) {
|
||||
companion object {
|
||||
internal const val ALERT_COOLDOWN_MS = 5 * 60_000L
|
||||
internal const val EMPTY_REARM_DELAY_MS = 30_000L
|
||||
}
|
||||
|
||||
private var previousPeerCount = 0
|
||||
private var isArmed = true
|
||||
private var emptySinceMillis: Long? = null
|
||||
|
||||
init {
|
||||
require(alertCooldownMs >= 0) { "alertCooldownMs must not be negative" }
|
||||
require(emptyRearmDelayMs >= 0) { "emptyRearmDelayMs must not be negative" }
|
||||
}
|
||||
|
||||
fun update(peerCount: Int, isAppInBackground: Boolean): PeerAvailabilityAction {
|
||||
require(peerCount >= 0) { "peerCount must not be negative" }
|
||||
|
||||
val now = nowMillis()
|
||||
if (peerCount == 0) {
|
||||
if (previousPeerCount > 0 || emptySinceMillis == null) {
|
||||
emptySinceMillis = now
|
||||
}
|
||||
previousPeerCount = 0
|
||||
return PeerAvailabilityAction.CLEAR
|
||||
}
|
||||
|
||||
val transitionedFromEmpty = previousPeerCount == 0
|
||||
previousPeerCount = peerCount
|
||||
if (!transitionedFromEmpty) return PeerAvailabilityAction.NONE
|
||||
|
||||
if (!isArmed) {
|
||||
val emptySince = emptySinceMillis
|
||||
val remainedEmptyLongEnough =
|
||||
emptySince != null && now - emptySince >= emptyRearmDelayMs
|
||||
if (!remainedEmptyLongEnough) {
|
||||
emptySinceMillis = null
|
||||
return PeerAvailabilityAction.NONE
|
||||
}
|
||||
isArmed = true
|
||||
}
|
||||
emptySinceMillis = null
|
||||
|
||||
val lastAlertAt = alertHistory.lastAlertAtMillis
|
||||
val cooldownElapsed =
|
||||
lastAlertAt == null || now - lastAlertAt >= alertCooldownMs
|
||||
return if (isAppInBackground && cooldownElapsed) {
|
||||
PeerAvailabilityAction.SHOW
|
||||
} else {
|
||||
PeerAvailabilityAction.NONE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called only after NotificationManager accepts the post. Failed or disabled posts
|
||||
* do not consume the cooldown or require the mesh to re-arm.
|
||||
*/
|
||||
fun markAlertShown() {
|
||||
alertHistory.lastAlertAtMillis = nowMillis()
|
||||
isArmed = false
|
||||
}
|
||||
}
|
||||
|
||||
internal interface PeerAvailabilityTextProvider {
|
||||
fun title(): String
|
||||
fun body(peerCount: Int): String
|
||||
}
|
||||
|
||||
private class AndroidPeerAvailabilityTextProvider(
|
||||
private val context: Context
|
||||
) : PeerAvailabilityTextProvider {
|
||||
override fun title(): String {
|
||||
return context.getString(R.string.notification_active_peers_title)
|
||||
}
|
||||
|
||||
override fun body(peerCount: Int): String {
|
||||
return if (peerCount == 1) {
|
||||
context.getString(R.string.notification_active_peers_one)
|
||||
} else {
|
||||
context.getString(R.string.notification_active_peers_many, peerCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Owns the user-visible "bitchatters nearby" notification independently of the UI delegate.
|
||||
*/
|
||||
internal class PeerAvailabilityNotifier(
|
||||
private val context: Context,
|
||||
private val notificationManager: NotificationManagerCompat =
|
||||
NotificationManagerCompat.from(context),
|
||||
private val tracker: PeerAvailabilityTracker = PeerAvailabilityTracker(
|
||||
SharedPreferencesPeerAvailabilityAlertHistory(context)
|
||||
),
|
||||
private val textProvider: PeerAvailabilityTextProvider =
|
||||
AndroidPeerAvailabilityTextProvider(context),
|
||||
private val canPostNotifications: () -> Boolean = {
|
||||
notificationManager.areNotificationsEnabled() &&
|
||||
(
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
|
||||
ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
)
|
||||
}
|
||||
) {
|
||||
companion object {
|
||||
internal const val CHANNEL_ID = "bitchat_peer_availability_notifications"
|
||||
internal const val NOTIFICATION_ID = 997
|
||||
private const val TAG = "PeerAvailability"
|
||||
}
|
||||
|
||||
init {
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
fun onPeerCountChanged(peerCount: Int, isAppInBackground: Boolean) {
|
||||
when (tracker.update(peerCount, isAppInBackground)) {
|
||||
PeerAvailabilityAction.NONE -> Unit
|
||||
PeerAvailabilityAction.CLEAR -> clear()
|
||||
PeerAvailabilityAction.SHOW -> showNotification(peerCount)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
textProvider.title(),
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
enableVibration(true)
|
||||
setShowBadge(false)
|
||||
}
|
||||
val systemManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
systemManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun showNotification(peerCount: Int) {
|
||||
if (!canPostNotifications()) {
|
||||
Log.i(TAG, "Skipping peer availability notification because notifications are disabled")
|
||||
return
|
||||
}
|
||||
|
||||
val openAppIntent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
NOTIFICATION_ID,
|
||||
openAppIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(textProvider.title())
|
||||
.setContentText(textProvider.body(peerCount))
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_SOCIAL)
|
||||
.setShowWhen(true)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.build()
|
||||
|
||||
try {
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
tracker.markAlertShown()
|
||||
Log.i(TAG, "Posted peer availability notification for $peerCount peer(s)")
|
||||
} catch (error: SecurityException) {
|
||||
Log.w(TAG, "Notification permission changed before peer alert was posted", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -30,7 +30,6 @@ import com.bitchat.android.protocol.BitchatPacket
|
||||
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.bitchat.android.util.NotificationIntervalManager
|
||||
import kotlinx.coroutines.delay
|
||||
import java.util.Date
|
||||
import kotlin.random.Random
|
||||
@ -152,8 +151,7 @@ class ChatViewModel(
|
||||
)
|
||||
private val notificationManager = NotificationManager(
|
||||
application.applicationContext,
|
||||
NotificationManagerCompat.from(application.applicationContext),
|
||||
NotificationIntervalManager()
|
||||
NotificationManagerCompat.from(application.applicationContext)
|
||||
)
|
||||
|
||||
private val verificationHandler = VerificationHandler(
|
||||
|
||||
@ -109,7 +109,6 @@ class MeshDelegateHandler(
|
||||
private suspend fun processPeerUpdate(mergedPeers: List<String>) {
|
||||
state.setConnectedPeers(mergedPeers)
|
||||
state.setIsConnected(mergedPeers.isNotEmpty())
|
||||
notificationManager.showActiveUserNotification(mergedPeers)
|
||||
|
||||
// Flush router outbox for any peers that just connected (and their noiseHex aliases)
|
||||
runCatching { com.bitchat.android.services.MessageRouter.tryGetInstance()?.onPeersUpdated(mergedPeers) }
|
||||
|
||||
@ -23,7 +23,6 @@ import com.bitchat.android.R
|
||||
import com.bitchat.android.service.ConversationNotificationReceiver
|
||||
import com.bitchat.android.services.ContactDirectory
|
||||
import com.bitchat.android.services.ConversationListPreferences
|
||||
import com.bitchat.android.util.NotificationIntervalManager
|
||||
import java.util.Collections
|
||||
import java.util.WeakHashMap
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
@ -36,12 +35,10 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
* - Support for mention notifications in geohash chats
|
||||
* - Support for first message notifications in geohash chats
|
||||
* - Proper notification management and cleanup
|
||||
* - Active peers notification
|
||||
*/
|
||||
class NotificationManager(
|
||||
private val context: Context,
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
private val notificationIntervalManager: NotificationIntervalManager
|
||||
private val notificationManager: NotificationManagerCompat
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@ -54,9 +51,7 @@ class NotificationManager(
|
||||
private const val GEOHASH_NOTIFICATION_REQUEST_CODE = 2000
|
||||
private const val SUMMARY_NOTIFICATION_ID = 999
|
||||
private const val GEOHASH_SUMMARY_NOTIFICATION_ID = 998
|
||||
private const val ACTIVE_PEERS_NOTIFICATION_ID = 997
|
||||
private const val MAX_MESSAGES_IN_NOTIFICATION = 25
|
||||
private const val ACTIVE_PEERS_NOTIFICATION_TIME_INTERVAL = com.bitchat.android.util.AppConstants.UI.ACTIVE_PEERS_NOTIFICATION_INTERVAL_MS
|
||||
|
||||
// Intent extras for notification handling
|
||||
const val EXTRA_OPEN_PRIVATE_CHAT = "open_private_chat"
|
||||
@ -220,22 +215,6 @@ class NotificationManager(
|
||||
}
|
||||
}
|
||||
|
||||
fun showActiveUserNotification(peers: List<String>) {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val activePeerNotificationIntervalExceeded =
|
||||
(currentTime - notificationIntervalManager.lastNetworkNotificationTime) > ACTIVE_PEERS_NOTIFICATION_TIME_INTERVAL
|
||||
val newPeers = peers - notificationIntervalManager.recentlySeenPeers
|
||||
if (isAppInBackground && activePeerNotificationIntervalExceeded && newPeers.isNotEmpty()) {
|
||||
Log.d(TAG, "Showing notification for active peers")
|
||||
showNotificationForActivePeers(peers.size)
|
||||
notificationIntervalManager.setLastNetworkNotificationTime(currentTime)
|
||||
notificationIntervalManager.recentlySeenPeers.addAll(newPeers)
|
||||
} else {
|
||||
Log.d(TAG, "Skipping notification - app in foreground or it has been less than 5 minutes since last active peer notification")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNotificationForSender(senderPeerID: String) {
|
||||
val notifications = pendingNotifications[senderPeerID] ?: return
|
||||
if (notifications.isEmpty()) return
|
||||
@ -430,41 +409,6 @@ class NotificationManager(
|
||||
)
|
||||
}
|
||||
|
||||
private fun showNotificationForActivePeers(peersSize: Int) {
|
||||
// Create intent to open the app
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
ACTIVE_PEERS_NOTIFICATION_ID,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
// Build notification content
|
||||
val contentTitle = context.getString(R.string.notification_active_peers_title)
|
||||
val contentText = if (peersSize == 1) {
|
||||
context.getString(R.string.notification_active_peers_one)
|
||||
} else {
|
||||
context.getString(R.string.notification_active_peers_many, peersSize)
|
||||
}
|
||||
|
||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(contentTitle)
|
||||
.setContentText(contentText)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setShowWhen(true)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
|
||||
notifySafely(ACTIVE_PEERS_NOTIFICATION_ID, builder.build())
|
||||
Log.d(TAG, "Displayed notification for $contentTitle with ID $ACTIVE_PEERS_NOTIFICATION_ID")
|
||||
}
|
||||
private fun showSummaryNotification() {
|
||||
if (pendingNotifications.isEmpty()) return
|
||||
|
||||
|
||||
@ -125,7 +125,6 @@ object AppConstants {
|
||||
const val BASE_FONT_SIZE_SP: Int = 14
|
||||
const val MESSAGE_DEDUP_TIMEOUT_MS: Long = 30_000L
|
||||
const val SYSTEM_EVENT_DEDUP_TIMEOUT_MS: Long = 5_000L
|
||||
const val ACTIVE_PEERS_NOTIFICATION_INTERVAL_MS: Long = 300_000L
|
||||
const val ACTION_FORCE_FINISH: String = "com.bitchat.android.ACTION_FORCE_FINISH"
|
||||
const val PERMISSION_FORCE_FINISH: String = "com.bitchat.android.permission.FORCE_FINISH"
|
||||
}
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
package com.bitchat.android.util
|
||||
|
||||
class NotificationIntervalManager {
|
||||
private var _lastNetworkNotificationTime = 0L
|
||||
val lastNetworkNotificationTime: Long
|
||||
get() = _lastNetworkNotificationTime
|
||||
|
||||
val recentlySeenPeers: MutableSet<String> = mutableSetOf()
|
||||
|
||||
fun setLastNetworkNotificationTime(notificationTime: Long) {
|
||||
_lastNetworkNotificationTime = notificationTime
|
||||
}
|
||||
}
|
||||
@ -93,8 +93,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
||||
// Service-level notification manager for background (no-UI) DMs
|
||||
private val serviceNotificationManager = com.bitchat.android.ui.NotificationManager(
|
||||
context.applicationContext,
|
||||
androidx.core.app.NotificationManagerCompat.from(context.applicationContext),
|
||||
com.bitchat.android.util.NotificationIntervalManager()
|
||||
androidx.core.app.NotificationManagerCompat.from(context.applicationContext)
|
||||
)
|
||||
|
||||
// Wi-Fi Aware transport
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">و%1$d إضافية</string>
|
||||
<string name="notification_messages_from_people">%1$d رسالة من %2$d أشخاص</string>
|
||||
<string name="notification_more_conversations">و%1$d محادثات أخرى</string>
|
||||
<string name="notification_active_peers_title">👥 مستخدمون قريبون!</string>
|
||||
<string name="notification_active_peers_title">مستخدمون قريبون!</string>
|
||||
<string name="notification_active_peers_one">شخص واحد قريب</string>
|
||||
<string name="notification_active_peers_many">%1$d أشخاص قريبون</string>
|
||||
<string name="notification_new_messages">رسائل جديدة</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">এবং %1$d আরো</string>
|
||||
<string name="notification_messages_from_people">%2$d জন থেকে %1$d বার্তা</string>
|
||||
<string name="notification_more_conversations">এবং %1$d আরো কথোপকথন</string>
|
||||
<string name="notification_active_peers_title">👥 কাছাকাছি bitchatter!</string>
|
||||
<string name="notification_active_peers_title">কাছাকাছি bitchatter!</string>
|
||||
<string name="notification_active_peers_one">কাছাকাছি ১ জন</string>
|
||||
<string name="notification_active_peers_many">কাছাকাছি %1$d জন</string>
|
||||
<string name="notification_new_messages">নতুন বার্তা</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">und %1$d weitere</string>
|
||||
<string name="notification_messages_from_people">%1$d Nachrichten von %2$d Personen</string>
|
||||
<string name="notification_more_conversations">und %1$d weitere Unterhaltungen</string>
|
||||
<string name="notification_active_peers_title">👥 bitchatter in der Nähe!</string>
|
||||
<string name="notification_active_peers_title">bitchatter in der Nähe!</string>
|
||||
<string name="notification_active_peers_one">1 Person in der Nähe</string>
|
||||
<string name="notification_active_peers_many">%1$d Personen in der Nähe</string>
|
||||
<string name="notification_new_messages">Neue Nachrichten</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">y %1$d más</string>
|
||||
<string name="notification_messages_from_people">%1$d mensajes de %2$d personas</string>
|
||||
<string name="notification_more_conversations">y %1$d conversaciones más</string>
|
||||
<string name="notification_active_peers_title">👥 ¡bitchatters cerca!</string>
|
||||
<string name="notification_active_peers_title">¡bitchatters cerca!</string>
|
||||
<string name="notification_active_peers_one">1 persona cerca</string>
|
||||
<string name="notification_active_peers_many">%1$d personas cerca</string>
|
||||
<string name="notification_new_messages">Nuevos mensajes</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">و %1$d مورد دیگر</string>
|
||||
<string name="notification_messages_from_people">%1$d پیام از %2$d نفر</string>
|
||||
<string name="notification_more_conversations">و %1$d گفتوگوی دیگر</string>
|
||||
<string name="notification_active_peers_title">👥 کاربران bitchat در نزدیکی!</string>
|
||||
<string name="notification_active_peers_title">کاربران bitchat در نزدیکی!</string>
|
||||
<string name="notification_active_peers_one">۱ نفر در نزدیکی</string>
|
||||
<string name="notification_active_peers_many">%1$d نفر در نزدیکی</string>
|
||||
<string name="notification_new_messages">پیامهای جدید</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">at %1$d pa</string>
|
||||
<string name="notification_messages_from_people">%1$d mensahe mula sa %2$d tao</string>
|
||||
<string name="notification_more_conversations">at %1$d pang usapan</string>
|
||||
<string name="notification_active_peers_title">👥 mga bitchatter na malapit!</string>
|
||||
<string name="notification_active_peers_title">mga bitchatter na malapit!</string>
|
||||
<string name="notification_active_peers_one">1 tao sa paligid</string>
|
||||
<string name="notification_active_peers_many">%1$d tao sa paligid</string>
|
||||
<string name="notification_new_messages">Mga bagong mensahe</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">et %1$d de plus</string>
|
||||
<string name="notification_messages_from_people">%1$d messages de %2$d personnes</string>
|
||||
<string name="notification_more_conversations">et %1$d conversations de plus</string>
|
||||
<string name="notification_active_peers_title">👥 bitchatters à proximité !</string>
|
||||
<string name="notification_active_peers_title">bitchatters à proximité !</string>
|
||||
<string name="notification_active_peers_one">1 personne à proximité</string>
|
||||
<string name="notification_active_peers_many">%1$d personnes à proximité</string>
|
||||
<string name="notification_new_messages">Nouveaux messages</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">और %1$d</string>
|
||||
<string name="notification_messages_from_people">%2$d लोगों से %1$d संदेश</string>
|
||||
<string name="notification_more_conversations">और %1$d वार्तालाप</string>
|
||||
<string name="notification_active_peers_title">👥 पास में उपयोगकर्ता!</string>
|
||||
<string name="notification_active_peers_title">पास में उपयोगकर्ता!</string>
|
||||
<string name="notification_active_peers_one">पास में 1 व्यक्ति</string>
|
||||
<string name="notification_active_peers_many">पास में %1$d लोग</string>
|
||||
<string name="notification_new_messages">नए संदेश</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">dan %1$d lagi</string>
|
||||
<string name="notification_messages_from_people">%1$d pesan dari %2$d orang</string>
|
||||
<string name="notification_more_conversations">dan %1$d percakapan lagi</string>
|
||||
<string name="notification_active_peers_title">👥 bitchatter terdekat!</string>
|
||||
<string name="notification_active_peers_title">bitchatter terdekat!</string>
|
||||
<string name="notification_active_peers_one">1 orang terdekat</string>
|
||||
<string name="notification_active_peers_many">%1$d orang terdekat</string>
|
||||
<string name="notification_new_messages">Pesan baru</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">e altri %1$d</string>
|
||||
<string name="notification_messages_from_people">%1$d messaggi da %2$d persone</string>
|
||||
<string name="notification_more_conversations">e altre %1$d conversazioni</string>
|
||||
<string name="notification_active_peers_title">👥 bitchatter nelle vicinanze!</string>
|
||||
<string name="notification_active_peers_title">bitchatter nelle vicinanze!</string>
|
||||
<string name="notification_active_peers_one">1 persona nei dintorni</string>
|
||||
<string name="notification_active_peers_many">%1$d persone nei dintorni</string>
|
||||
<string name="notification_new_messages">Nuovi messaggi</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">ほか %1$d 件</string>
|
||||
<string name="notification_messages_from_people">%2$d 人からの %1$d 件のメッセージ</string>
|
||||
<string name="notification_more_conversations">ほか %1$d 会話</string>
|
||||
<string name="notification_active_peers_title">👥 近くに bitchatter !</string>
|
||||
<string name="notification_active_peers_title">近くに bitchatter !</string>
|
||||
<string name="notification_active_peers_one">近くに 1 人</string>
|
||||
<string name="notification_active_peers_many">近くに %1$d 人</string>
|
||||
<string name="notification_new_messages">新着メッセージ</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">და კიდევ %1$d</string>
|
||||
<string name="notification_messages_from_people">%2$d ადამიანიდან %1$d შეტყობინება</string>
|
||||
<string name="notification_more_conversations">და კიდევ %1$d საუბარი</string>
|
||||
<string name="notification_active_peers_title">👥 bitchatter-ები ახლოს!</string>
|
||||
<string name="notification_active_peers_title">bitchatter-ები ახლოს!</string>
|
||||
<string name="notification_active_peers_one">1 ადამიანი ახლოს</string>
|
||||
<string name="notification_active_peers_many">ახლოს %1$d ადამიანი</string>
|
||||
<string name="notification_new_messages">ახალი შეტყობინებები</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">그리고 %1$d개 더</string>
|
||||
<string name="notification_messages_from_people">%2$d명에게서 %1$d개의 메시지</string>
|
||||
<string name="notification_more_conversations">그리고 %1$d개의 대화 더</string>
|
||||
<string name="notification_active_peers_title">👥 근처 bitchatter!</string>
|
||||
<string name="notification_active_peers_title">근처 bitchatter!</string>
|
||||
<string name="notification_active_peers_one">근처 1명</string>
|
||||
<string name="notification_active_peers_many">근처 %1$d명</string>
|
||||
<string name="notification_new_messages">새 메시지</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">ary %1$d hafa</string>
|
||||
<string name="notification_messages_from_people">%1$d hafatra avy amin\'ny olona %2$d</string>
|
||||
<string name="notification_more_conversations">ary %1$d resaka hafa</string>
|
||||
<string name="notification_active_peers_title">👥 bitchatter akaiky!</string>
|
||||
<string name="notification_active_peers_title">bitchatter akaiky!</string>
|
||||
<string name="notification_active_peers_one">Olona 1 manodidina</string>
|
||||
<string name="notification_active_peers_many">Olona %1$d manodidina</string>
|
||||
<string name="notification_new_messages">Hafatra vaovao</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">र %1$d थप</string>
|
||||
<string name="notification_messages_from_people">%1$d सन्देश %2$d जनाबाट</string>
|
||||
<string name="notification_more_conversations">र %1$d अरू कुराकानी</string>
|
||||
<string name="notification_active_peers_title">👥 नजिकै bitchatter हरू!</string>
|
||||
<string name="notification_active_peers_title">नजिकै bitchatter हरू!</string>
|
||||
<string name="notification_active_peers_one">१ जना नजिकै</string>
|
||||
<string name="notification_active_peers_many">%1$d जना नजिकै</string>
|
||||
<string name="notification_new_messages">नयाँ सन्देश</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">en %1$d meer</string>
|
||||
<string name="notification_messages_from_people">%1$d berichten van %2$d personen</string>
|
||||
<string name="notification_more_conversations">en %1$d meer conversaties</string>
|
||||
<string name="notification_active_peers_title">👥 bitchatters in de buurt!</string>
|
||||
<string name="notification_active_peers_title">bitchatters in de buurt!</string>
|
||||
<string name="notification_active_peers_one">1 persoon in de buurt</string>
|
||||
<string name="notification_active_peers_many">%1$d personen in de buurt</string>
|
||||
<string name="notification_new_messages">Nieuwe berichten</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">تے %1$d ہور</string>
|
||||
<string name="notification_messages_from_people">%2$d لوکان کولوں %1$d پیغام</string>
|
||||
<string name="notification_more_conversations">تے %1$d ہور گلاں</string>
|
||||
<string name="notification_active_peers_title">👥 نیڑے bitchatter!</string>
|
||||
<string name="notification_active_peers_title">نیڑے bitchatter!</string>
|
||||
<string name="notification_active_peers_one">نیڑے 1 بندہ</string>
|
||||
<string name="notification_active_peers_many">نیڑے %1$d بندے</string>
|
||||
<string name="notification_new_messages">نویں پیغام</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">e mais %1$d</string>
|
||||
<string name="notification_messages_from_people">%1$d mensagens de %2$d pessoas</string>
|
||||
<string name="notification_more_conversations">e mais %1$d conversas</string>
|
||||
<string name="notification_active_peers_title">👥 pessoas do bitchat por perto!</string>
|
||||
<string name="notification_active_peers_title">pessoas do bitchat por perto!</string>
|
||||
<string name="notification_active_peers_one">1 pessoa por perto</string>
|
||||
<string name="notification_active_peers_many">%1$d pessoas por perto</string>
|
||||
<string name="notification_new_messages">Novas mensagens</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">e mais %1$d</string>
|
||||
<string name="notification_messages_from_people">%1$d mensagens de %2$d pessoas</string>
|
||||
<string name="notification_more_conversations">e mais %1$d conversas</string>
|
||||
<string name="notification_active_peers_title">👥 bitchatters por perto!</string>
|
||||
<string name="notification_active_peers_title">bitchatters por perto!</string>
|
||||
<string name="notification_active_peers_one">1 pessoa por perto</string>
|
||||
<string name="notification_active_peers_many">%1$d pessoas por perto</string>
|
||||
<string name="notification_new_messages">Novas mensagens</string>
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
<string name="notification_summary_more">и ещё %1$d</string>
|
||||
<string name="notification_messages_from_people">%1$d сообщений от %2$d людей</string>
|
||||
<string name="notification_more_conversations">и ещё %1$d бесед</string>
|
||||
<string name="notification_active_peers_title">👥 bitchatter рядом!</string>
|
||||
<string name="notification_active_peers_title">bitchatter рядом!</string>
|
||||
<string name="notification_active_peers_one">Рядом 1 человек</string>
|
||||
<string name="notification_active_peers_many">Рядом %1$d человек</string>
|
||||
<string name="notification_new_messages">Новые сообщения</string>
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
<string name="notification_summary_more">och %1$d till</string>
|
||||
<string name="notification_messages_from_people">%1$d meddelanden från %2$d personer</string>
|
||||
<string name="notification_more_conversations">och %1$d fler konversationer</string>
|
||||
<string name="notification_active_peers_title">👥 bitchatter i närheten!</string>
|
||||
<string name="notification_active_peers_title">bitchatter i närheten!</string>
|
||||
<string name="notification_active_peers_one">1 person i närheten</string>
|
||||
<string name="notification_active_peers_many">%1$d personer i närheten</string>
|
||||
<string name="notification_new_messages">Nya meddelanden</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">และอีก %1$d</string>
|
||||
<string name="notification_messages_from_people">ข้อความ %1$d จาก %2$d คน</string>
|
||||
<string name="notification_more_conversations">และการสนทนาอีก %1$d รายการ</string>
|
||||
<string name="notification_active_peers_title">👥 มีผู้ใช้ใกล้คุณ!</string>
|
||||
<string name="notification_active_peers_title">มีผู้ใช้ใกล้คุณ!</string>
|
||||
<string name="notification_active_peers_one">มี 1 คนใกล้คุณ</string>
|
||||
<string name="notification_active_peers_many">มี %1$d คนใกล้คุณ</string>
|
||||
<string name="notification_new_messages">ข้อความใหม่</string>
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
<string name="notification_summary_more">ve %1$d daha</string>
|
||||
<string name="notification_messages_from_people">%2$d kişiden %1$d mesaj</string>
|
||||
<string name="notification_more_conversations">ve %1$d sohbet daha</string>
|
||||
<string name="notification_active_peers_title">👥 yakında bitchatter’lar!</string>
|
||||
<string name="notification_active_peers_title">yakında bitchatter’lar!</string>
|
||||
<string name="notification_active_peers_one">Yakında 1 kişi</string>
|
||||
<string name="notification_active_peers_many">Yakında %1$d kişi</string>
|
||||
<string name="notification_new_messages">Yeni mesajlar</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">اور %1$d مزید</string>
|
||||
<string name="notification_messages_from_people">%2$d لوگوں سے %1$d پیغامات</string>
|
||||
<string name="notification_more_conversations">اور %1$d مزید گفتگو</string>
|
||||
<string name="notification_active_peers_title">👥 قریب میں bitchatter!</string>
|
||||
<string name="notification_active_peers_title">قریب میں bitchatter!</string>
|
||||
<string name="notification_active_peers_one">قریب میں 1 شخص</string>
|
||||
<string name="notification_active_peers_many">قریب میں %1$d لوگ</string>
|
||||
<string name="notification_new_messages">نئے پیغامات</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">và %1$d khác</string>
|
||||
<string name="notification_messages_from_people">%1$d tin nhắn từ %2$d người</string>
|
||||
<string name="notification_more_conversations">và %1$d cuộc trò chuyện khác</string>
|
||||
<string name="notification_active_peers_title">👥 bitchatter gần đó!</string>
|
||||
<string name="notification_active_peers_title">bitchatter gần đó!</string>
|
||||
<string name="notification_active_peers_one">1 người gần đó</string>
|
||||
<string name="notification_active_peers_many">%1$d người gần đó</string>
|
||||
<string name="notification_new_messages">Tin nhắn mới</string>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="notification_summary_more">以及另外 %1$d 条</string>
|
||||
<string name="notification_messages_from_people">来自 %2$d 位的 %1$d 条消息</string>
|
||||
<string name="notification_more_conversations">以及另外 %1$d 个会话</string>
|
||||
<string name="notification_active_peers_title">👥 附近的 bitchatter!</string>
|
||||
<string name="notification_active_peers_title">附近的 bitchatter!</string>
|
||||
<string name="notification_active_peers_one">附近 1 人</string>
|
||||
<string name="notification_active_peers_many">附近 %1$d 人</string>
|
||||
<string name="notification_new_messages">新消息</string>
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
<string name="notification_summary_more">and %1$d more</string>
|
||||
<string name="notification_messages_from_people">%1$d messages from %2$d people</string>
|
||||
<string name="notification_more_conversations">and %1$d more conversations</string>
|
||||
<string name="notification_active_peers_title">👥 bitchatters nearby!</string>
|
||||
<string name="notification_active_peers_title">bitchatters nearby!</string>
|
||||
<string name="notification_active_peers_one">1 person around</string>
|
||||
<string name="notification_active_peers_many">%1$d people around</string>
|
||||
<string name="notification_new_messages">New Messages</string>
|
||||
|
||||
@ -1,117 +0,0 @@
|
||||
package com.bitchat
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.bitchat.android.ui.NotificationManager
|
||||
import com.bitchat.android.util.NotificationIntervalManager
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.times
|
||||
import org.mockito.MockitoAnnotations
|
||||
import org.mockito.Spy
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.never
|
||||
import org.mockito.kotlin.verify
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class NotificationManagerTest {
|
||||
|
||||
private val context: Context = ApplicationProvider.getApplicationContext()
|
||||
private val notificationIntervalManager = NotificationIntervalManager()
|
||||
lateinit var notificationManager: NotificationManager
|
||||
private val notificationManagerCompat: NotificationManagerCompat = Mockito.mock(NotificationManagerCompat::class.java)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.openMocks(this)
|
||||
notificationManager = NotificationManager(
|
||||
context,
|
||||
notificationManagerCompat,
|
||||
notificationIntervalManager
|
||||
)
|
||||
}
|
||||
|
||||
@Ignore // Temporarily disabled due to Mockito final class issues
|
||||
@Test
|
||||
fun `when there are no active peers, do not send active peer notification`() {
|
||||
notificationManager.setAppBackgroundState(true)
|
||||
notificationManager.showActiveUserNotification(emptyList())
|
||||
verify(notificationManagerCompat, never()).notify(any(), any())
|
||||
}
|
||||
|
||||
@Ignore // Temporarily disabled due to Mockito final class issues
|
||||
@Test
|
||||
fun `when app is in foreground, do not send active peer notification`() {
|
||||
notificationManager.setAppBackgroundState(false)
|
||||
notificationManager.showActiveUserNotification(listOf("peer-1"))
|
||||
verify(notificationManagerCompat, never()).notify(any(), any())
|
||||
}
|
||||
|
||||
@Ignore // Temporarily disabled due to Mockito final class issues
|
||||
@Test
|
||||
fun `when there is an active peer, send notification`() {
|
||||
notificationManager.setAppBackgroundState(true)
|
||||
notificationManager.showActiveUserNotification(listOf("peer-1"))
|
||||
verify(notificationManagerCompat, times(1)).notify(any(), any())
|
||||
}
|
||||
|
||||
@Ignore // Temporarily disabled due to Mockito final class issues
|
||||
@Test
|
||||
fun `when there is an active peer but less than 5 minutes have passed since last notification, do not send notification`() {
|
||||
notificationManager.setAppBackgroundState(true)
|
||||
notificationManager.showActiveUserNotification(listOf("peer-1"))
|
||||
notificationManager.showActiveUserNotification(listOf("peer-2"))
|
||||
verify(notificationManagerCompat, times(1)).notify(any(), any())
|
||||
}
|
||||
|
||||
@Ignore // Temporarily disabled due to Mockito final class issues
|
||||
@Test
|
||||
fun `when there is an active peer and more than 5 minutes have passed since last notification, send notification`() {
|
||||
notificationManager.setAppBackgroundState(true)
|
||||
notificationManager.showActiveUserNotification(listOf("peer-1"))
|
||||
notificationIntervalManager.setLastNetworkNotificationTime(System.currentTimeMillis() - 301_000L)
|
||||
notificationManager.showActiveUserNotification(listOf("peer-2"))
|
||||
verify(notificationManagerCompat, times(2)).notify(any(), any())
|
||||
}
|
||||
|
||||
@Ignore // Temporarily disabled due to Mockito final class issues
|
||||
@Test
|
||||
fun `when there is a recently seen peer but no new active peers, no notification is sent`() {
|
||||
notificationManager.setAppBackgroundState(true)
|
||||
notificationIntervalManager.recentlySeenPeers.add("peer-1")
|
||||
notificationManager.showActiveUserNotification(emptyList())
|
||||
verify(notificationManagerCompat, times(0)).notify(any(), any())
|
||||
}
|
||||
|
||||
@Ignore // Temporarily disabled due to Mockito final class issues
|
||||
@Test
|
||||
fun `when an active peer is a recently seen peer, do not send notification`() {
|
||||
notificationManager.setAppBackgroundState(true)
|
||||
notificationIntervalManager.recentlySeenPeers.add("peer-1")
|
||||
notificationManager.showActiveUserNotification(listOf("peer-1"))
|
||||
verify(notificationManagerCompat, times(0)).notify(any(), any())
|
||||
}
|
||||
|
||||
@Ignore // Temporarily disabled due to Mockito final class issues
|
||||
@Test
|
||||
fun `when an active peer is a new peer, send notification`() {
|
||||
notificationManager.setAppBackgroundState(true)
|
||||
notificationIntervalManager.recentlySeenPeers.addAll(emptyList())
|
||||
notificationManager.showActiveUserNotification(listOf("peer-1"))
|
||||
verify(notificationManagerCompat, times(1)).notify(any(), any())
|
||||
}
|
||||
|
||||
@Ignore // Temporarily disabled due to Mockito final class issues
|
||||
@Test
|
||||
fun `when an active peer is a new peer and there are already multiple recently seen peers, send notification`() {
|
||||
notificationManager.setAppBackgroundState(true)
|
||||
notificationIntervalManager.recentlySeenPeers.addAll(listOf("peer-1", "peer-2"))
|
||||
notificationManager.showActiveUserNotification(listOf("peer-3"))
|
||||
verify(notificationManagerCompat, times(1)).notify(any(), any())
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package com.bitchat.android.onboarding
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [33])
|
||||
class OptionalPermissionRequestTest {
|
||||
private lateinit var application: Application
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
application = ApplicationProvider.getApplicationContext()
|
||||
application.getSharedPreferences("bitchat_permissions", Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.clear()
|
||||
.commit()
|
||||
shadowOf(application).denyPermissions(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `missing notification permission is offered once`() {
|
||||
val permissionManager = PermissionManager(application)
|
||||
|
||||
assertEquals(
|
||||
listOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
permissionManager.getUnrequestedOptionalPermissions()
|
||||
)
|
||||
|
||||
permissionManager.markOptionalPermissionsRequested(
|
||||
listOf(Manifest.permission.POST_NOTIFICATIONS)
|
||||
)
|
||||
|
||||
assertTrue(permissionManager.getUnrequestedOptionalPermissions().isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `granted notification permission is not requested`() {
|
||||
shadowOf(application).grantPermissions(Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
||||
assertTrue(PermissionManager(application).getUnrequestedOptionalPermissions().isEmpty())
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,267 @@
|
||||
package com.bitchat.android.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
class PeerAvailabilityTrackerTest {
|
||||
|
||||
@Test
|
||||
fun `repeated flapping produces only one alert during cooldown`() {
|
||||
val clock = MutableClock()
|
||||
val tracker = tracker(clock = clock)
|
||||
var showCount = 0
|
||||
|
||||
if (tracker.update(1, isAppInBackground = true) == PeerAvailabilityAction.SHOW) {
|
||||
showCount++
|
||||
tracker.markAlertShown()
|
||||
}
|
||||
|
||||
repeat(5) {
|
||||
clock.advance(1_000L)
|
||||
assertEquals(
|
||||
PeerAvailabilityAction.CLEAR,
|
||||
tracker.update(0, isAppInBackground = true)
|
||||
)
|
||||
clock.advance(PeerAvailabilityTracker.EMPTY_REARM_DELAY_MS)
|
||||
if (tracker.update(1, isAppInBackground = true) == PeerAvailabilityAction.SHOW) {
|
||||
showCount++
|
||||
tracker.markAlertShown()
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(1, showCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mesh must remain empty before notification re-arms`() {
|
||||
val clock = MutableClock()
|
||||
val tracker = tracker(
|
||||
clock = clock,
|
||||
alertCooldownMs = 0L
|
||||
)
|
||||
|
||||
assertEquals(PeerAvailabilityAction.SHOW, tracker.update(1, isAppInBackground = true))
|
||||
tracker.markAlertShown()
|
||||
|
||||
assertEquals(PeerAvailabilityAction.CLEAR, tracker.update(0, isAppInBackground = true))
|
||||
clock.advance(PeerAvailabilityTracker.EMPTY_REARM_DELAY_MS - 1)
|
||||
assertEquals(PeerAvailabilityAction.NONE, tracker.update(1, isAppInBackground = true))
|
||||
|
||||
assertEquals(PeerAvailabilityAction.CLEAR, tracker.update(0, isAppInBackground = true))
|
||||
clock.advance(PeerAvailabilityTracker.EMPTY_REARM_DELAY_MS)
|
||||
assertEquals(PeerAvailabilityAction.SHOW, tracker.update(1, isAppInBackground = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cooldown survives tracker recreation`() {
|
||||
val clock = MutableClock()
|
||||
val history = InMemoryAlertHistory()
|
||||
val originalTracker = tracker(clock = clock, history = history)
|
||||
|
||||
assertEquals(
|
||||
PeerAvailabilityAction.SHOW,
|
||||
originalTracker.update(1, isAppInBackground = true)
|
||||
)
|
||||
originalTracker.markAlertShown()
|
||||
|
||||
clock.advance(PeerAvailabilityTracker.ALERT_COOLDOWN_MS - 1)
|
||||
val restartedDuringCooldown = tracker(clock = clock, history = history)
|
||||
assertEquals(
|
||||
PeerAvailabilityAction.NONE,
|
||||
restartedDuringCooldown.update(1, isAppInBackground = true)
|
||||
)
|
||||
|
||||
clock.advance(1)
|
||||
val restartedAfterCooldown = tracker(clock = clock, history = history)
|
||||
assertEquals(
|
||||
PeerAvailabilityAction.SHOW,
|
||||
restartedAfterCooldown.update(1, isAppInBackground = true)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `foreground discovery is not replayed after app enters background`() {
|
||||
val tracker = tracker()
|
||||
|
||||
assertEquals(PeerAvailabilityAction.NONE, tracker.update(1, isAppInBackground = false))
|
||||
assertEquals(PeerAvailabilityAction.NONE, tracker.update(1, isAppInBackground = true))
|
||||
assertEquals(PeerAvailabilityAction.CLEAR, tracker.update(0, isAppInBackground = true))
|
||||
assertEquals(PeerAvailabilityAction.SHOW, tracker.update(1, isAppInBackground = true))
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun `negative peer count is rejected`() {
|
||||
tracker().update(-1, isAppInBackground = true)
|
||||
}
|
||||
|
||||
private fun tracker(
|
||||
clock: MutableClock = MutableClock(),
|
||||
history: InMemoryAlertHistory = InMemoryAlertHistory(),
|
||||
alertCooldownMs: Long = PeerAvailabilityTracker.ALERT_COOLDOWN_MS
|
||||
): PeerAvailabilityTracker {
|
||||
return PeerAvailabilityTracker(
|
||||
alertHistory = history,
|
||||
nowMillis = clock::now,
|
||||
alertCooldownMs = alertCooldownMs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [32])
|
||||
class PeerAvailabilityNotifierTest {
|
||||
private lateinit var context: Context
|
||||
private lateinit var systemNotificationManager: NotificationManager
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
systemNotificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
systemNotificationManager.cancelAll()
|
||||
context.getSharedPreferences(
|
||||
SharedPreferencesPeerAvailabilityAlertHistory.PREFERENCES_NAME,
|
||||
Context.MODE_PRIVATE
|
||||
).edit().clear().commit()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `background availability posts on dedicated channel`() {
|
||||
val textProvider = testTextProvider()
|
||||
val notifier = PeerAvailabilityNotifier(
|
||||
context = context,
|
||||
tracker = tracker(),
|
||||
textProvider = textProvider,
|
||||
canPostNotifications = { true }
|
||||
)
|
||||
|
||||
notifier.onPeerCountChanged(0, isAppInBackground = true)
|
||||
notifier.onPeerCountChanged(2, isAppInBackground = true)
|
||||
|
||||
val notification =
|
||||
shadowOf(systemNotificationManager).getNotification(
|
||||
PeerAvailabilityNotifier.NOTIFICATION_ID
|
||||
)
|
||||
assertNotNull(notification)
|
||||
assertEquals(PeerAvailabilityNotifier.CHANNEL_ID, notification.channelId)
|
||||
assertEquals(
|
||||
textProvider.title(),
|
||||
notification.extras.getString("android.title")
|
||||
)
|
||||
assertEquals(
|
||||
textProvider.body(2),
|
||||
notification.extras.getString("android.text")
|
||||
)
|
||||
assertTrue(notification.flags and Notification.FLAG_ONLY_ALERT_ONCE != 0)
|
||||
assertNotNull(
|
||||
systemNotificationManager.getNotificationChannel(PeerAvailabilityNotifier.CHANNEL_ID)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returning to zero cancels availability notification`() {
|
||||
val notifier = PeerAvailabilityNotifier(
|
||||
context = context,
|
||||
tracker = tracker(),
|
||||
textProvider = testTextProvider(),
|
||||
canPostNotifications = { true }
|
||||
)
|
||||
|
||||
notifier.onPeerCountChanged(1, isAppInBackground = true)
|
||||
notifier.onPeerCountChanged(0, isAppInBackground = true)
|
||||
|
||||
assertNull(
|
||||
shadowOf(systemNotificationManager).getNotification(
|
||||
PeerAvailabilityNotifier.NOTIFICATION_ID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `explicit clear cancels availability notification`() {
|
||||
val notifier = PeerAvailabilityNotifier(
|
||||
context = context,
|
||||
tracker = tracker(),
|
||||
textProvider = testTextProvider(),
|
||||
canPostNotifications = { true }
|
||||
)
|
||||
|
||||
notifier.onPeerCountChanged(1, isAppInBackground = true)
|
||||
notifier.clear()
|
||||
|
||||
assertNull(
|
||||
shadowOf(systemNotificationManager).getNotification(
|
||||
PeerAvailabilityNotifier.NOTIFICATION_ID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disabled notifications do not post`() {
|
||||
val history = InMemoryAlertHistory()
|
||||
val notifier = PeerAvailabilityNotifier(
|
||||
context = context,
|
||||
tracker = PeerAvailabilityTracker(history),
|
||||
textProvider = testTextProvider(),
|
||||
canPostNotifications = { false }
|
||||
)
|
||||
|
||||
notifier.onPeerCountChanged(1, isAppInBackground = true)
|
||||
|
||||
assertNull(
|
||||
shadowOf(systemNotificationManager).getNotification(
|
||||
PeerAvailabilityNotifier.NOTIFICATION_ID
|
||||
)
|
||||
)
|
||||
assertNull(history.lastAlertAtMillis)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shared preferences history persists last alert time`() {
|
||||
val history = SharedPreferencesPeerAvailabilityAlertHistory(context)
|
||||
history.lastAlertAtMillis = 42L
|
||||
|
||||
assertEquals(
|
||||
42L,
|
||||
SharedPreferencesPeerAvailabilityAlertHistory(context).lastAlertAtMillis ?: -1L
|
||||
)
|
||||
}
|
||||
|
||||
private fun tracker(): PeerAvailabilityTracker {
|
||||
return PeerAvailabilityTracker(InMemoryAlertHistory())
|
||||
}
|
||||
|
||||
private fun testTextProvider(): PeerAvailabilityTextProvider {
|
||||
return object : PeerAvailabilityTextProvider {
|
||||
override fun title(): String = "Bitchatters nearby"
|
||||
|
||||
override fun body(peerCount: Int): String = "$peerCount people around"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class InMemoryAlertHistory(
|
||||
override var lastAlertAtMillis: Long? = null
|
||||
) : PeerAvailabilityAlertHistory
|
||||
|
||||
private class MutableClock(
|
||||
private var currentMillis: Long = 1_000L
|
||||
) {
|
||||
fun now(): Long = currentMillis
|
||||
|
||||
fun advance(durationMillis: Long) {
|
||||
currentMillis += durationMillis
|
||||
}
|
||||
}
|
||||
@ -66,14 +66,12 @@ class MeshDelegateHandlerStateContractTest {
|
||||
|
||||
assertEquals(listOf("peer-a", "peer-b"), state.connectedPeers.value)
|
||||
assertTrue(state.isConnected.value)
|
||||
verify(notifications).showActiveUserNotification(listOf("peer-a", "peer-b"))
|
||||
verify(channels).cleanupDisconnectedMembers(listOf("peer-a", "peer-b"), "self")
|
||||
|
||||
handler.didUpdatePeerList(emptyList())
|
||||
|
||||
assertTrue(state.connectedPeers.value.isEmpty())
|
||||
assertFalse(state.isConnected.value)
|
||||
verify(notifications).showActiveUserNotification(emptyList())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user