Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.service)
implementation(libs.androidx.lifecycle.process)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.camerax.camera2)
implementation(libs.camerax.lifecycle)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,52 @@
package app.nectarlink.android

import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import app.nectarlink.android.banking.BankingPause
import app.nectarlink.android.core.Core
import app.nectarlink.android.elevated.Elevated
import app.nectarlink.android.ui.Preferences
import app.nectarlink.android.update.AppUpdater
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch

/** Starts the core with the process and keeps it for the app's lifetime. */
class NectarlinkApplication : Application() {
class NectarlinkApplication : Application(), DefaultLifecycleObserver {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

val core: Core by lazy { Core(this, scope) }

/** Updates from GitHub releases (release builds only). */
val updater: AppUpdater by lazy { AppUpdater(this) }

private var pendingStopJob: Job? = null

private val prefListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key == Preferences.BACKGROUND_SERVICE_ENABLED) {
if (Preferences.isBackgroundServiceEnabled(this)) {
pendingStopJob?.cancel()
pendingStopJob = null
} else {
if (!isInForeground()) {
scheduleStop()
}
}
}
}

override fun onCreate() {
super.onCreate()
super<Application>.onCreate()
// Before the core starts: its network code reads the phone's DNS servers.
app.nectarlink.android.core.AndroidContext.install(this)
// Before Elevated: it waits while paused for a banking app.
Expand All @@ -31,12 +57,66 @@ class NectarlinkApplication : Application() {
// What the phone offers PCs follows Elevated starting and stopping.
Elevated.onChange = { core.refreshNotificationAccess() }
Elevated.onClipboardChanged = { text, html -> core.onElevatedClipboardChanged(text, html) }
android.view.Choreographer.getInstance().postFrameCallback {
scope.launch { core.start() }

ProcessLifecycleOwner.get().lifecycle.addObserver(this)
getSharedPreferences("appearance", Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(prefListener)

if (Preferences.isBackgroundServiceEnabled(this)) {
android.view.Choreographer.getInstance().postFrameCallback {
scope.launch { core.start() }
}
scope.launch {
delay(120)
core.start()
}
}
scope.launch {
kotlinx.coroutines.delay(120)
core.start()
}

override fun onStart(owner: LifecycleOwner) {
pendingStopJob?.cancel()
pendingStopJob = null
core.start()
}

override fun onStop(owner: LifecycleOwner) {
if (!Preferences.isBackgroundServiceEnabled(this)) {
scheduleStop()
}
}

fun onBackgroundServiceDisabled() {
if (!isInForeground()) {
scheduleStop()
}
}

private fun isInForeground(): Boolean =
ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)

private fun isStopEligible(): Boolean =
!isInForeground() && !Preferences.isBackgroundServiceEnabled(this)

private fun scheduleStop() {
pendingStopJob?.cancel()
pendingStopJob = scope.launch(Dispatchers.IO) {
delay(STOP_DELAY_MS)
if (!isStopEligible()) return@launch

while (core.busy.value) {
core.busy.first { !it }
delay(STOP_IDLE_DEBOUNCE_MS)
if (!isStopEligible()) return@launch
}

if (isStopEligible()) {
core.stop()
}
}
}

companion object {
const val STOP_DELAY_MS = 5_000L
const val STOP_IDLE_DEBOUNCE_MS = 2_000L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ class PcCompanionService : CompanionDeviceService() {

private fun wakeAndReconnect() {
val appContext = applicationContext ?: return
if (!app.nectarlink.android.ui.Preferences.isBackgroundServiceEnabled(appContext)) {
return
}
ConnectionService.start(appContext)
(appContext as? NectarlinkApplication)?.core?.reconnectNow()
}
Expand Down
Loading
Loading