Skip to content

Commit

Permalink
Feature/clients ongoing trades notifications (android) (#176)
Browse files Browse the repository at this point in the history
* - land user in open trades when clicking notification implemented for bisq connect android

* refactor: extract foreground detector into a separate platform based code controller

* - android client working (Except kill app case for the reasons in discussion) - improved logging for iOS
  • Loading branch information
rodvar authored Jan 28, 2025
1 parent 3928f1c commit 88795ca
Show file tree
Hide file tree
Showing 15 changed files with 204 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package network.bisq.mobile.client

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.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

/**
* Redefinition to be able to access activity for trading notifications click handling
*/
class AndroidClientMainPresenter(openTradesNotificationService: OpenTradesNotificationService,
tradesServiceFacade: TradesServiceFacade,
webSocketClientProvider: WebSocketClientProvider,
applicationBootstrapFacade: ApplicationBootstrapFacade,
offersServiceFacade: OffersServiceFacade,
marketPriceServiceFacade: MarketPriceServiceFacade,
settingsServiceFacade: SettingsServiceFacade, urlLauncher: UrlLauncher
) : ClientMainPresenter(
openTradesNotificationService, tradesServiceFacade, webSocketClientProvider, applicationBootstrapFacade,
offersServiceFacade, marketPriceServiceFacade, settingsServiceFacade, urlLauncher
) {
init {
openTradesNotificationService.notificationServiceController.activityClassForIntents = MainActivity::class.java
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package network.bisq.mobile.client

import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Build
Expand All @@ -17,6 +18,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 @@ -29,6 +31,14 @@ class MainActivity : ComponentActivity() {
MainPresenter.init()
}

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

intent?.getStringExtra("destination")?.let { destination ->
Routes.fromString(destination)?.let { presenter.navigateToTab(it) }
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
presenter.attachView(this)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package network.bisq.mobile.client.di

import network.bisq.mobile.client.AndroidClientMainPresenter
import network.bisq.mobile.client.service.user_profile.ClientCatHashService
import network.bisq.mobile.domain.AndroidUrlLauncher
import network.bisq.mobile.domain.UrlLauncher
import network.bisq.mobile.domain.service.AppForegroundController
import network.bisq.mobile.domain.service.ForegroundDetector
import network.bisq.mobile.domain.service.notifications.controller.NotificationServiceController
import network.bisq.mobile.presentation.MainPresenter
import network.bisq.mobile.presentation.ui.AppPresenter
import network.bisq.mobile.service.AndroidClientCatHashService
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.bind
Expand All @@ -16,4 +22,11 @@ val androidClientModule = module {
val filesDir = context.filesDir.absolutePath
AndroidClientCatHashService(context, filesDir)
} bind ClientCatHashService::class

single<AppForegroundController> { AppForegroundController(androidContext()) } bind ForegroundDetector::class
single<NotificationServiceController> {
NotificationServiceController(get())
}

single<MainPresenter> { AndroidClientMainPresenter(get(), get(), get(), get(), get(), get(), get(), get()) } bind AppPresenter::class
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ import network.bisq.mobile.domain.service.settings.SettingsServiceFacade
import network.bisq.mobile.domain.service.trades.TradesServiceFacade
import network.bisq.mobile.presentation.MainPresenter

/**
* Node main presenter has a very different setup than the rest of the apps (bisq2 core dependencies)
*/
class NodeMainPresenter(
urlLauncher: UrlLauncher,
openTradesNotificationService: OpenTradesNotificationService,
private val tradesServiceFacade: TradesServiceFacade,
private val openTradesNotificationService: OpenTradesNotificationService,
private val provider: AndroidApplicationService.Provider,
private val androidMemoryReportService: AndroidMemoryReportService,
private val applicationBootstrapFacade: ApplicationBootstrapFacade,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package network.bisq.mobile.domain.di

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

val serviceModule = module {
single<NotificationServiceController> { NotificationServiceController(androidContext()) }
single<AppForegroundController> { AppForegroundController(androidContext()) } bind ForegroundDetector::class
single<NotificationServiceController> { NotificationServiceController(get()) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package network.bisq.mobile.domain.service

import android.app.Activity
import android.app.Application
import android.content.Context
import android.os.Bundle
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import network.bisq.mobile.domain.utils.Logging

@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class AppForegroundController(val context: Context) : ForegroundDetector, Logging {
private val _isForeground = MutableStateFlow(false)
override val isForeground: StateFlow<Boolean> = _isForeground

init {
(context.applicationContext as Application).registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityResumed(activity: Activity) {
onAppWillEnterForeground()
}

override fun onActivityPaused(activity: Activity) {
onAppDidEnterBackground()
}

// Other lifecycle methods can be left empty
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
})
}


private fun onAppDidEnterBackground() {
log.d("App is in foreground -> false")
_isForeground.value = false
}

private fun onAppWillEnterForeground() {
log.d("App is in foreground -> true")
_isForeground.value = true
}

}
Original file line number Diff line number Diff line change
@@ -1,58 +1,39 @@
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.core.app.NotificationCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import network.bisq.mobile.domain.service.AppForegroundController
import network.bisq.mobile.domain.service.BisqForegroundService
import network.bisq.mobile.domain.utils.Logging

/**
* Controller interacting with the bisq service
*/
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class NotificationServiceController (private val context: Context): ServiceController, Logging {
actual class NotificationServiceController (private val appForegroundController: AppForegroundController): ServiceController, Logging {

private val context = appForegroundController.context

companion object {
const val SERVICE_NAME = "Bisq Service"
}

private val serviceScope = CoroutineScope(SupervisorJob())
private val observerJobs = mutableMapOf<StateFlow<*>, Job>()
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) {
isForeground = true
}

override fun onActivityPaused(activity: Activity) {
isForeground = false
}

// Other lifecycle methods can be left empty
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
})
}
var defaultDestination = "tab_my_trades" // TODO minor refactor move this hardcode out of here and into client leaf code }

/**
* Starts the service in the appropiate mode based on the current device running Android API
Expand Down Expand Up @@ -108,15 +89,16 @@ actual class NotificationServiceController (private val context: Context): Servi

// TODO support for on click and decide if we block on foreground
actual fun pushNotification(title: String, message: String) {
// if (isForeground) {
// log.w { "Skipping notification since app is in the foreground" }
// } else {
if (isAppInForeground()) {
log.w { "Skipping notification since app is in the foreground" }
return
}

// 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", "tab_my_trades") // Add extras to navigate to a specific screen
putExtra("destination", defaultDestination) // Add extras to navigate to a specific screen
}

// Create a PendingIntent to handle the notification click
Expand Down Expand Up @@ -159,7 +141,7 @@ actual class NotificationServiceController (private val context: Context): Servi
}

actual fun isAppInForeground(): Boolean {
TODO("Not yet implemented")
return appForegroundController.isForeground.value
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ class WebSocketClient(
} catch (e: Exception) {
log.e(e) { "Exception ocurred whilst listening for WS messages - triggering reconnect" }
} finally {
log.d { "Not listining for WS messages anymore" }
log.d { "Not listening for WS messages anymore - launching reconnect" }
reconnect()
}

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

import kotlinx.coroutines.flow.StateFlow

interface ForegroundDetector {
val isForeground: StateFlow<Boolean>
}

@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
expect class AppForegroundController
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ package network.bisq.mobile.domain.di

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

val iosClientModule = module {
single<UrlLauncher> { IOSUrlLauncher() }
single<AppForegroundController> { AppForegroundController() } bind ForegroundDetector::class
single<NotificationServiceController> {
NotificationServiceController().apply {
NotificationServiceController(get()).apply {
this.registerBackgroundTask()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package network.bisq.mobile.domain.service

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import network.bisq.mobile.domain.utils.Logging
import platform.Foundation.NSNotificationCenter
import platform.UIKit.UIApplicationDidEnterBackgroundNotification
import platform.UIKit.UIApplicationWillEnterForegroundNotification

@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class AppForegroundController : ForegroundDetector, Logging {
private val _isForeground = MutableStateFlow(true)
override val isForeground: StateFlow<Boolean> = _isForeground

init {
val notificationCenter = NSNotificationCenter.defaultCenter
notificationCenter.addObserverForName(
name = UIApplicationDidEnterBackgroundNotification,
`object` = null,
queue = null
) { notification ->
onAppDidEnterBackground()
}
notificationCenter.addObserverForName(
name = UIApplicationWillEnterForegroundNotification,
`object` = null,
queue = null
) { notification ->
onAppWillEnterForeground()
}
}

private fun onAppDidEnterBackground() {
log.d {"App is in foreground -> false" }
_isForeground.value = false

}

private fun onAppWillEnterForeground() {
log.d {"App is in foreground -> true" }
_isForeground.value = true
}
}
Loading

0 comments on commit 88795ca

Please sign in to comment.