Skip to content

Commit

Permalink
- make sure notifications are clickable on android
Browse files Browse the repository at this point in the history
 - notification takes user to open trades tab
 - refactor main notification open trade code into its own service

[Ticket: X]
  • Loading branch information
rodvar committed Jan 23, 2025
1 parent 402d73d commit 902be94
Show file tree
Hide file tree
Showing 15 changed files with 120 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package network.bisq.mobile.android.node

import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
Expand All @@ -14,6 +15,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.core.content.ContextCompat
import network.bisq.mobile.presentation.MainPresenter
import network.bisq.mobile.presentation.ui.App
import network.bisq.mobile.presentation.ui.navigation.Routes
import org.koin.android.ext.android.inject

class MainActivity : ComponentActivity() {
Expand All @@ -26,6 +28,15 @@ class MainActivity : ComponentActivity() {
MainPresenter.init()
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)

val destination = intent?.getStringExtra("destination")
if (destination == "my_trades") {
presenter.navigateToTab(Routes.TabOpenTradeList)
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
presenter.attachView(this)
Expand All @@ -36,6 +47,7 @@ class MainActivity : ComponentActivity() {

handleDynamicPermissions()
}

override fun onStart() {
super.onStart()
presenter.onStart()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,34 @@ package network.bisq.mobile.android.node.presentation

import android.app.Activity
import network.bisq.mobile.android.node.AndroidApplicationService
import network.bisq.mobile.android.node.MainActivity
import network.bisq.mobile.android.node.service.AndroidMemoryReportService
import network.bisq.mobile.domain.UrlLauncher
import network.bisq.mobile.domain.service.bootstrap.ApplicationBootstrapFacade
import network.bisq.mobile.domain.service.controller.NotificationServiceController
import network.bisq.mobile.domain.service.market_price.MarketPriceServiceFacade
import network.bisq.mobile.domain.service.notifications.OpenTradesNotificationService
import network.bisq.mobile.domain.service.offers.OffersServiceFacade
import network.bisq.mobile.domain.service.settings.SettingsServiceFacade
import network.bisq.mobile.domain.service.trades.TradesServiceFacade
import network.bisq.mobile.presentation.MainPresenter

class NodeMainPresenter(
notificationServiceController: NotificationServiceController,
urlLauncher: UrlLauncher,
tradesServiceFacade: TradesServiceFacade,
private val tradesServiceFacade: TradesServiceFacade,
private val openTradesNotificationService: OpenTradesNotificationService,
private val provider: AndroidApplicationService.Provider,
private val androidMemoryReportService: AndroidMemoryReportService,
private val applicationBootstrapFacade: ApplicationBootstrapFacade,
private val settingsServiceFacade: SettingsServiceFacade,
private val offersServiceFacade: OffersServiceFacade,
private val marketPriceServiceFacade: MarketPriceServiceFacade,
) : MainPresenter(tradesServiceFacade, notificationServiceController, urlLauncher) {
) : MainPresenter(openTradesNotificationService, urlLauncher) {

private var applicationServiceCreated = false

init {
openTradesNotificationService.notificationServiceController.activityClassForIntents = MainActivity::class.java
}
override fun onViewAttached() {
super.onViewAttached()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.asImageBitmap
import com.russhwolf.settings.Settings
import kotlinx.serialization.Serializable
import network.bisq.mobile.domain.service.notifications.controller.NotificationServiceController
import java.io.ByteArrayOutputStream
import java.text.DecimalFormat
import java.util.Locale
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package network.bisq.mobile.domain.di

import network.bisq.mobile.domain.service.controller.NotificationServiceController
import network.bisq.mobile.domain.service.notifications.controller.NotificationServiceController
import org.koin.dsl.module
import org.koin.android.ext.koin.androidContext

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package network.bisq.mobile.domain.service.controller
package network.bisq.mobile.domain.service.notifications.controller

import android.app.Activity
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.compose.runtime.collectAsState
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
Expand All @@ -33,6 +33,8 @@ actual class NotificationServiceController (private val context: Context): Servi
private var isForeground = false
private var isRunning = false

var activityClassForIntents = context::class.java

init {
(context.applicationContext as Application).registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityResumed(activity: Activity) {
Expand Down Expand Up @@ -110,13 +112,30 @@ actual class NotificationServiceController (private val context: Context): Servi
// if (isForeground) {
// log.w { "Skipping notification since app is in the foreground" }
// } else {

// Create an intent that brings the user back to the app
val intent = Intent(context, activityClassForIntents).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
// flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra("destination", "my_trades") // Add extras to navigate to a specific screen
}

// Create a PendingIntent to handle the notification click
val pendingIntent = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE // FLAG_IMMUTABLE is required on Android 12+
)
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationCompat.Builder(context, BisqForegroundService.CHANNEL_ID)
.setContentTitle(title)
.setContentText(message)
.setSmallIcon(android.R.drawable.ic_notification_overlay)
.setPriority(NotificationCompat.PRIORITY_DEFAULT) // For android previous to O
.setOngoing(true)
// .setOngoing(true)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.build()
notificationManager.notify(BisqForegroundService.PUSH_NOTIFICATION_ID, notification)
log.d {"Pushed notification: $title: $message" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import network.bisq.mobile.domain.data.repository.BisqStatsRepository
import network.bisq.mobile.domain.data.repository.SettingsRepository
import network.bisq.mobile.domain.data.repository.UserRepository
import network.bisq.mobile.domain.getPlatformSettings
import network.bisq.mobile.domain.service.notifications.OpenTradesNotificationService
import org.koin.dsl.module

val domainModule = module {
Expand All @@ -27,4 +28,7 @@ val domainModule = module {
single<BisqStatsRepository> { BisqStatsRepository() }
single<SettingsRepository> { SettingsRepository(get()) }
single<UserRepository> { UserRepository(get()) }

// Services
single<OpenTradesNotificationService> { OpenTradesNotificationService(get(), get()) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package network.bisq.mobile.domain.service.notifications

import network.bisq.mobile.domain.data.replicated.presentation.open_trades.TradeItemPresentationModel
import network.bisq.mobile.domain.service.notifications.controller.NotificationServiceController
import network.bisq.mobile.domain.service.offers.OffersServiceFacade
import network.bisq.mobile.domain.service.trades.TradesServiceFacade
import network.bisq.mobile.domain.utils.Logging

class OpenTradesNotificationService(
val notificationServiceController: NotificationServiceController,
private val tradesServiceFacade: TradesServiceFacade): Logging {

fun launchNotificationService() {
notificationServiceController.startService()
runCatching {
notificationServiceController.registerObserver(tradesServiceFacade.openTradeItems) { newValue ->
log.d { "open trades in total: ${newValue.size}" }
newValue.sortedByDescending { it.bisqEasyTradeModel.takeOfferDate }
.forEach { trade ->
onTradeUpdate(trade)
}
}
}.onFailure {
log.e(it) { "Failed to register observer" }
}
}

fun stopNotificationService() {
notificationServiceController.unregisterObserver(tradesServiceFacade.openTradeItems)
// TODO unregister all ?
notificationServiceController.stopService()
}

/**
* Register to observe open trade state. Unregister when the trade concludes
* Triggers push notifications
*/
private fun onTradeUpdate(trade: TradeItemPresentationModel) {
log.d { "open trade: $trade" }
notificationServiceController.registerObserver(trade.bisqEasyTradeModel.tradeState) {
log.d { "Open trade State Changed to: $it" }
if (OffersServiceFacade.isTerminalNode(it)) {
notificationServiceController.unregisterObserver(trade.bisqEasyTradeModel.tradeState)
notificationServiceController.pushNotification(
"Trade [${trade.shortTradeId}] completed",
"Your trade with ${trade.peersUserName} has finished as ${it}"
)
} else {
notificationServiceController.pushNotification(
"Trade [${trade.shortTradeId}] activity",
"Your trade with ${trade.peersUserName} needs your attention"
)
}

}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package network.bisq.mobile.domain.service.controller
package network.bisq.mobile.domain.service.notifications.controller

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.StateFlow

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package network.bisq.mobile.domain.service.controller
package network.bisq.mobile.domain.service.notifications.controller

import kotlinx.coroutines.flow.StateFlow

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package network.bisq.mobile.domain.di

import network.bisq.mobile.domain.IOSUrlLauncher
import network.bisq.mobile.domain.UrlLauncher
import network.bisq.mobile.domain.service.controller.NotificationServiceController
import network.bisq.mobile.domain.service.notifications.controller.NotificationServiceController
import org.koin.dsl.module

val iosClientModule = module {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package network.bisq.mobile.domain.service.controller
package network.bisq.mobile.domain.service.notifications.controller

import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.coroutines.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ import kotlinx.coroutines.launch
import network.bisq.mobile.client.websocket.WebSocketClientProvider
import network.bisq.mobile.domain.UrlLauncher
import network.bisq.mobile.domain.service.bootstrap.ApplicationBootstrapFacade
import network.bisq.mobile.domain.service.controller.NotificationServiceController
import network.bisq.mobile.domain.service.market_price.MarketPriceServiceFacade
import network.bisq.mobile.domain.service.notifications.OpenTradesNotificationService
import network.bisq.mobile.domain.service.offers.OffersServiceFacade
import network.bisq.mobile.domain.service.settings.SettingsServiceFacade
import network.bisq.mobile.domain.service.trades.TradesServiceFacade
import network.bisq.mobile.presentation.MainPresenter

class ClientMainPresenter(
notificationServiceController: NotificationServiceController,
tradesServiceFacade: TradesServiceFacade,
openTradesNotificationService: OpenTradesNotificationService,
private val tradesServiceFacade: TradesServiceFacade,
private val webSocketClientProvider: WebSocketClientProvider,
private val applicationBootstrapFacade: ApplicationBootstrapFacade,
private val offersServiceFacade: OffersServiceFacade,
private val marketPriceServiceFacade: MarketPriceServiceFacade,
private val settingsServiceFacade: SettingsServiceFacade,
urlLauncher: UrlLauncher
) : MainPresenter(tradesServiceFacade, notificationServiceController, urlLauncher) {
) : MainPresenter(openTradesNotificationService, urlLauncher) {

override fun onViewAttached() {
super.onViewAttached()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ abstract class BasePresenter(private val rootPresenter: MainPresenter?): ViewPre
/**
* Navigates to the given tab route inside the main presentation, with default parameters.
*/
protected fun navigateToTab(destination: Routes, saveStateOnPopUp: Boolean = true, shouldLaunchSingleTop: Boolean = true, shouldRestoreState: Boolean = true) {
fun navigateToTab(destination: Routes, saveStateOnPopUp: Boolean = true, shouldLaunchSingleTop: Boolean = true, shouldRestoreState: Boolean = true) {
log.d { "Navigating to tab ${destination.name} "}
uiScope.launch(Dispatchers.Main) {
getRootTabNavController().navigate(destination.name) {
getRootTabNavController().graph.startDestinationRoute?.let { route ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@ import kotlinx.coroutines.flow.StateFlow
import network.bisq.mobile.android.node.BuildNodeConfig
import network.bisq.mobile.client.shared.BuildConfig
import network.bisq.mobile.domain.UrlLauncher
import network.bisq.mobile.domain.data.replicated.presentation.open_trades.TradeItemPresentationModel
import network.bisq.mobile.domain.getDeviceLanguageCode
import network.bisq.mobile.domain.getPlatformInfo
import network.bisq.mobile.domain.service.controller.NotificationServiceController
import network.bisq.mobile.domain.service.offers.OffersServiceFacade
import network.bisq.mobile.domain.service.trades.TradesServiceFacade
import network.bisq.mobile.domain.service.notifications.OpenTradesNotificationService
import network.bisq.mobile.domain.setupUncaughtExceptionHandler
import network.bisq.mobile.presentation.ui.AppPresenter
import kotlin.jvm.JvmStatic
Expand All @@ -22,8 +19,7 @@ import kotlin.jvm.JvmStatic
* Main Presenter as an example of implementation for now.
*/
open class MainPresenter(
protected val tradesServiceFacade: TradesServiceFacade,
private val notificationServiceController: NotificationServiceController,
private val openTradesNotificationService: OpenTradesNotificationService,
private val urlLauncher: UrlLauncher
) : BasePresenter(null), AppPresenter {
companion object {
Expand Down Expand Up @@ -57,46 +53,7 @@ open class MainPresenter(
@CallSuper
override fun onViewAttached() {
super.onViewAttached()
launchNotificationService()
}

private fun launchNotificationService() {
notificationServiceController.startService()
runCatching {
notificationServiceController.registerObserver(tradesServiceFacade.openTradeItems) { newValue ->
log.d { "open trades in total: ${newValue.size}" }
newValue.sortedByDescending { it.bisqEasyTradeModel.takeOfferDate }
.forEach { trade ->
onTradeUpdate(trade)
}
}
}.onFailure {
log.e(it) { "Failed to register observer" }
}
}

/**
* Register to observe open trade state. Unregister when the trade concludes
* Triggers push notifications
*/
private fun onTradeUpdate(trade: TradeItemPresentationModel) {
log.d { "open trade: $trade" }
notificationServiceController.registerObserver(trade.bisqEasyTradeModel.tradeState) {
log.d { "Open trade State Changed to: $it" }
if (OffersServiceFacade.isTerminalNode(it)) {
notificationServiceController.unregisterObserver(trade.bisqEasyTradeModel.tradeState)
pushNotification(
"Trade [${trade.shortTradeId}] completed",
"Your trade with ${trade.peersUserName} has finished as ${it}"
)
} else {
pushNotification(
"Trade [${trade.shortTradeId}] activity",
"Your trade with ${trade.peersUserName} needs your attention"
)
}

}
openTradesNotificationService.launchNotificationService()
}

// Toggle action
Expand All @@ -118,10 +75,6 @@ open class MainPresenter(
return tabNavController
}

public final override fun pushNotification(title: String, content: String) {
notificationServiceController.pushNotification(title, content)
}

final override fun navigateToUrl(url: String) {
urlLauncher.openUrl(url)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import network.bisq.mobile.i18n.I18nSupport
import network.bisq.mobile.presentation.ViewPresenter
import network.bisq.mobile.presentation.ui.components.SwipeBackIOSNavigationHandler
import network.bisq.mobile.presentation.ui.helpers.RememberPresenterLifecycle
import network.bisq.mobile.presentation.ui.navigation.Routes
import org.koin.compose.koinInject

import network.bisq.mobile.presentation.ui.navigation.graph.RootNavGraph
Expand Down

0 comments on commit 902be94

Please sign in to comment.