Skip to content

Commit

Permalink
Feature/resilient background service (#89)
Browse files Browse the repository at this point in the history
* Bisq Background/Foreground Service

 - Controller and service definitions
 - Implementation for Android
 - Permissions setup including dynamic permissions for OS that command
   so

* - notifications and silent notification when there is nothing to say

* - lower polling interval whilst on dev

* - fix issues on ios, setup permissions
 - more memory for idea intellij backend (fleet config)

* - added logs for ios notifications logic

* Fix - At this stage foreground notification work

However, background notification don't work because of ID of app changes

* - merge nish fixes for iOS

* - merge work from nish

* - fix crashes on real devices when no connectivity

* - check for tasks regularly (ios impl) + fix debug log calls

* - flag to turn on/off testing code, turning off by default before merge

---------

Co-authored-by: Nish <[email protected]>
  • Loading branch information
rodvar and Nish authored Dec 10, 2024
1 parent 849875d commit 5f508b1
Show file tree
Hide file tree
Showing 33 changed files with 625 additions and 36 deletions.
11 changes: 11 additions & 0 deletions androidClient/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"/>

<application
android:name=".MainApplication"
Expand All @@ -21,6 +24,14 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!-- OS services -->
<service
android:name="network.bisq.mobile.domain.service.BisqForegroundService"
android:exported="false"
android:permission="android.permission.FOREGROUND_SERVICE"
android:foregroundServiceType="remoteMessaging">
</service>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
package network.bisq.mobile.client

import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
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 org.koin.android.ext.android.inject

class MainActivity : ComponentActivity() {
private val presenter: MainPresenter by inject()

// TODO probably better to handle from presenter once the user reach home
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -23,6 +33,8 @@ class MainActivity : ComponentActivity() {
setContent {
App()
}

handleDynamicPermissions()
}

override fun onStart() {
Expand Down Expand Up @@ -50,6 +62,38 @@ class MainActivity : ComponentActivity() {
presenter.onDestroy()
super.onDestroy()
}

private fun handleDynamicPermissions() {
requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
// Permission granted, proceed with posting notifications
} else {
// Permission denied, show a message to the user
Toast.makeText(this, "Permission denied. Notifications won't be sent.", Toast.LENGTH_SHORT).show()
}
}

// Call the method to check and request permission in APIs where its mandatory
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
checkAndRequestNotificationPermission()
}
}

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private fun checkAndRequestNotificationPermission() {
// Check if the permission is granted
if (ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED) {
// Permission already granted, proceed with posting notifications
} else {
// Request permission if not granted
requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
}
}
}

@Preview
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.app.Application
import network.bisq.mobile.client.di.androidClientModule
import network.bisq.mobile.client.di.clientModule
import network.bisq.mobile.domain.di.domainModule
import network.bisq.mobile.domain.di.serviceModule
import network.bisq.mobile.presentation.di.presentationModule
import org.koin.android.ext.koin.androidContext

Expand All @@ -15,7 +16,7 @@ class MainApplication: Application() {

startKoin {
androidContext(this@MainApplication)
modules(listOf(domainModule, presentationModule, clientModule, androidClientModule))
modules(listOf(domainModule, serviceModule, presentationModule, clientModule, androidClientModule))
}
}
}
11 changes: 11 additions & 0 deletions androidNode/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"/>

<application
android:name="network.bisq.mobile.android.node.MainApplication"
Expand All @@ -20,6 +23,14 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!-- OS services -->
<service
android:name="network.bisq.mobile.domain.service.BisqForegroundService"
android:exported="false"
android:permission="android.permission.FOREGROUND_SERVICE"
android:foregroundServiceType="remoteMessaging">
</service>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
package network.bisq.mobile.android.node

import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
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 org.koin.android.ext.android.inject

class MainActivity : ComponentActivity() {
private val presenter : MainPresenter by inject()

// TODO probably better to handle from presenter once the user reach home
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
presenter.attachView(this)

setContent {
App()
}
}

handleDynamicPermissions()
}
override fun onStart() {
super.onStart()
presenter.onStart()
Expand All @@ -46,6 +57,39 @@ class MainActivity : ComponentActivity() {
presenter.onDestroy()
super.onDestroy()
}

private fun handleDynamicPermissions() {
requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
// Permission granted, proceed with posting notifications
} else {
// Permission denied, show a message to the user
Toast.makeText(this, "Permission denied. Notifications won't be sent.", Toast.LENGTH_SHORT).show()
}
}

// Call the method to check and request permission in APIs where its mandatory
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
checkAndRequestNotificationPermission()
}
}

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private fun checkAndRequestNotificationPermission() {
// Check if the permission is granted
if (ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED) {
// Permission already granted, proceed with posting notifications
} else {
// Request permission if not granted
requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
}
}

}

@Preview
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import bisq.common.facades.android.AndroidJdkFacade
import bisq.common.network.AndroidEmulatorLocalhostFacade
import network.bisq.mobile.android.node.di.androidNodeModule
import network.bisq.mobile.domain.di.domainModule
import network.bisq.mobile.domain.di.serviceModule
import network.bisq.mobile.presentation.di.presentationModule
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.koin.android.ext.koin.androidContext
Expand Down Expand Up @@ -40,7 +41,7 @@ class MainApplication : Application() {
startKoin {
androidContext(this@MainApplication)
// order is important, last one is picked for each interface/class key
modules(listOf(domainModule, presentationModule, androidNodeModule))
modules(listOf(domainModule, serviceModule, presentationModule, androidNodeModule))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ val androidNodeModule = module {
single<OfferbookServiceFacade> { NodeOfferbookServiceFacade(get(), get()) }
// this line showcases both, the possibility to change behaviour of the app by changing one definition
// and binding the same obj to 2 different abstractions
single<MainPresenter> { NodeMainPresenter(get(), get(), get(), get(), get()) } bind AppPresenter::class
single<MainPresenter> { NodeMainPresenter(get(), get(), get(), get(), get(), get()) } bind AppPresenter::class

single<IOnboardingPresenter> { OnBoardingNodePresenter(get()) } bind IOnboardingPresenter::class
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,12 @@ class NodeMarketPriceServiceFacade(private val applicationService: AndroidApplic
private fun observeSelectedMarket() {
selectedMarketPin?.unbind()
selectedMarketPin = marketPriceService.selectedMarket.addObserver { market ->
_marketPriceItem.value = MarketPriceItem(toReplicatedMarket(market))
updatePrice()
try {
_marketPriceItem.value = MarketPriceItem(toReplicatedMarket(market))
updatePrice()
} catch (e: Exception) {
log.e("Failed to update market item", e)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ class NodeSelectedOfferbookMarketService(
private fun observeSelectedChannel() {
selectedChannelPin =
bisqEasyOfferbookChannelSelectionService.selectedChannel.addObserver { selectedChannel ->
this.selectedChannel = selectedChannel as BisqEasyOfferbookChannel
marketPriceService.setSelectedMarket(selectedChannel.market)
this.selectedChannel = selectedChannel as BisqEasyOfferbookChannel?
marketPriceService.setSelectedMarket(selectedChannel?.market)
applySelectedOfferbookMarket()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import android.app.Activity
import network.bisq.mobile.android.node.AndroidApplicationService
import network.bisq.mobile.android.node.service.AndroidMemoryReportService
import network.bisq.mobile.domain.data.repository.main.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.offerbook.OfferbookServiceFacade
import network.bisq.mobile.presentation.MainPresenter

class NodeMainPresenter(
notificationServiceController: NotificationServiceController,
private val provider: AndroidApplicationService.Provider,
private val androidMemoryReportService: AndroidMemoryReportService,
private val applicationBootstrapFacade: ApplicationBootstrapFacade,
private val offerbookServiceFacade: OfferbookServiceFacade,
private val marketPriceServiceFacade: MarketPriceServiceFacade
) : MainPresenter() {
) : MainPresenter(notificationServiceController) {

private var applicationServiceCreated = false
override fun onViewAttached() {
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ androidx-activityCompose = "1.9.2"

androidx-appcompat = "1.7.0"
androidx-constraintlayout = "2.1.4"
androidx-core = "1.13.1"
androidx-core-ktx = "1.13.1"
androidx-espresso-core = "3.6.1"
androidx-material = "1.12.0"
Expand Down Expand Up @@ -92,6 +93,7 @@ androidx-test-compose = { group = "androidx.compose.ui", name = "ui-test-junit4-
androidx-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "androidx-test-compose-ver" }

#kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
androidx-core = { group = "androidx.core", name = "core", version.ref = "androidx-core" }
#androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
roboelectric = { group = "org.robolectric", name = "robolectric", version.ref = "roboelectric" }
androidx-test = { group = "androidx.test", name = "core", version.ref = "androidx-test" }
Expand Down
13 changes: 11 additions & 2 deletions iosClient/iosClient/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>network.bisq.mobile.iosUC4273Y485</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
Expand All @@ -20,13 +26,16 @@
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
</array>
<key>UILaunchScreen</key>
<dict/>
<key>UIRequiredDeviceCapabilities</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ class LifecycleAwareComposeViewController: UIViewController {
init(presenter: MainPresenter) {
self.presenter = presenter
super.init(nibName: nil, bundle: nil)
presenter.attachView(view: self)
}

required init?(coder: NSCoder) {
Expand Down Expand Up @@ -64,6 +63,7 @@ class LifecycleAwareComposeViewController: UIViewController {

// Notify the child view controller that it was moved to a parent
mainViewController.didMove(toParent: self)
presenter.attachView(view: self)
}

// Equivalent to `onDestroy` in Android for final cleanup
Expand Down
4 changes: 3 additions & 1 deletion iosClient/iosClient/iosClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import presentation

@main
struct iosClient: App {

init() {
DependenciesProviderHelper().doInitKoin()
}
Expand All @@ -12,4 +13,5 @@ struct iosClient: App {
ContentView()
}
}
}

}
6 changes: 6 additions & 0 deletions shared/domain/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ kotlin {
implementation(libs.kotlinx.coroutines.test)
implementation(libs.koin.test)
}
androidMain.dependencies {
implementation(libs.androidx.core)

implementation(libs.koin.core)
implementation(libs.koin.android)
}
androidUnitTest.dependencies {
implementation(libs.mock.io)
implementation(libs.kotlin.test.junit.v180)
Expand Down
Loading

0 comments on commit 5f508b1

Please sign in to comment.