Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 8s
Renovate / renovate (push) Successful in 1m11s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Successful in 18m4s
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Failing after 1m33s
Beta — Codeberg pre-release / detect (push) Successful in 5s
Beta — Codeberg pre-release / beta (push) Skipped
Stable 1.2.0, after 1.2.0-beta.1 and beta.2. - Version bumped to 1.2.0 (1020099); the CHANGELOG entries moved from Unreleased to 1.2.0. - What's New for every shipped locale, now including cs-CZ. - Full store listing (title, short and full description) for every locale; new for cs-CZ, ar and fr-FR. Play only gets these through the `listing` lane. - `main` is merged in with the branch side kept. #81 and the Weblate updates already came in through the #88 squash, and the only lines unique to `main` were string keys that #88 removed. The #75 fix landed after beta.2, so no reporter has tested it yet. Closes #72, closes #78, closes #75, closes #92 Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de> Reviewed-on: https://codeberg.org/jlmakiola/agendula/pulls/103
130 lines
5.6 KiB
Kotlin
130 lines
5.6 KiB
Kotlin
package de.jeanlucmakiola.agendula
|
|
|
|
import android.app.Application
|
|
import androidx.hilt.work.HiltWorkerFactory
|
|
import androidx.lifecycle.ProcessLifecycleOwner
|
|
import androidx.work.Configuration
|
|
import dagger.hilt.EntryPoint
|
|
import dagger.hilt.InstallIn
|
|
import dagger.hilt.android.EntryPointAccessors
|
|
import dagger.hilt.android.HiltAndroidApp
|
|
import dagger.hilt.components.SingletonComponent
|
|
import de.jeanlucmakiola.agendula.data.di.ApplicationScope
|
|
import de.jeanlucmakiola.agendula.data.di.ChannelRefresher
|
|
import de.jeanlucmakiola.agendula.data.reminders.ReminderMaintenanceWorker
|
|
import de.jeanlucmakiola.agendula.data.reminders.ReminderScheduler
|
|
import de.jeanlucmakiola.agendula.data.reminders.TaskNotifier
|
|
import de.jeanlucmakiola.agendula.data.tasks.ProviderResolver
|
|
import de.jeanlucmakiola.agendula.data.tasks.StartupGate
|
|
import de.jeanlucmakiola.agendula.data.tasks.provider.ProviderSnapshotSource
|
|
import de.jeanlucmakiola.agendula.data.tasks.room.DatabaseCheckpoint
|
|
import de.jeanlucmakiola.agendula.widget.TaskWidgetUpdater
|
|
import de.jeanlucmakiola.floret.crash.CrashConfig
|
|
import de.jeanlucmakiola.floret.crash.CrashReporter
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.launch
|
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
import javax.inject.Inject
|
|
|
|
/**
|
|
* Application entry point. Registered as android:name=".AgendulaApp". Besides
|
|
* Hilt init, it kicks off a reminder re-sync on launch (independent of any UI),
|
|
* so alarms reflect tasks synced while the app was closed.
|
|
*/
|
|
@HiltAndroidApp
|
|
class AgendulaApp : Application(), Configuration.Provider {
|
|
|
|
/**
|
|
* Lets `@HiltWorker` workers take injected dependencies. The manifest removes
|
|
* WorkManager's default initializer so this one is used instead; without both
|
|
* halves, a worker with a constructor argument fails to instantiate at
|
|
* runtime rather than at build time.
|
|
*/
|
|
@Inject lateinit var workerFactory: HiltWorkerFactory
|
|
|
|
override val workManagerConfiguration: Configuration
|
|
get() = Configuration.Builder()
|
|
.setWorkerFactory(workerFactory)
|
|
.build()
|
|
|
|
override fun onCreate() {
|
|
super.onCreate()
|
|
// Install first thing so startup crashes are captured too (privacy-
|
|
// respecting, on-device; the user submits the report by hand).
|
|
CrashReporter.install(
|
|
this,
|
|
CrashConfig(
|
|
appLabel = getString(R.string.app_name),
|
|
newIssueUrl = getString(R.string.report_issue_url),
|
|
chooseIssueUrl = getString(R.string.report_issue_url),
|
|
),
|
|
)
|
|
val entryPoint = EntryPointAccessors.fromApplication(this, AppEntryPoint::class.java)
|
|
val scheduler = entryPoint.reminderScheduler()
|
|
// Mirror the stored storage mode into ProviderResolver and import a
|
|
// v0.3.x install's tasks, both before anything reads a store.
|
|
val startupGate = entryPoint.startupGate()
|
|
val scope = entryPoint.applicationScope()
|
|
// An alarm is armed off whichever store was active when it was scheduled,
|
|
// so a switch has to rebuild the set. Armed only once startup's own
|
|
// null -> stored transition is past, which the launch sync below covers.
|
|
val started = AtomicBoolean(false)
|
|
entryPoint.providerResolver().onModeChanged {
|
|
if (started.get()) {
|
|
scope.launch { runCatching { scheduler.sync() } }
|
|
runCatching { entryPoint.taskNotifier().refreshChannel() }
|
|
}
|
|
}
|
|
startupGate.start()
|
|
refreshNotificationChannels()
|
|
ProcessLifecycleOwner.get().lifecycle.addObserver(entryPoint.databaseCheckpoint())
|
|
scope.launch {
|
|
// Wait for the stored mode and the import to land first. Rescheduling
|
|
// alarms against whichever store autoMode happens to pick would arm
|
|
// them off the wrong one — or off an empty one, mid-import.
|
|
runCatching {
|
|
startupGate.awaitReady()
|
|
started.set(true)
|
|
scheduler.sync()
|
|
}
|
|
runCatching { entryPoint.taskWidgetUpdater().start() }
|
|
runCatching { entryPoint.providerSnapshotSource().start() }
|
|
runCatching { ReminderMaintenanceWorker.schedule(this@AgendulaApp) }
|
|
}
|
|
}
|
|
|
|
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
|
|
super.onConfigurationChanged(newConfig)
|
|
refreshNotificationChannels()
|
|
// Language or dark mode changed: the widget's strings and list colours follow.
|
|
runCatching {
|
|
EntryPointAccessors.fromApplication(this, AppEntryPoint::class.java).taskWidgetUpdater().requestRefresh()
|
|
}
|
|
}
|
|
|
|
/** Channel names are stored by the system in whatever language created them. */
|
|
private fun refreshNotificationChannels() {
|
|
val entryPoint = EntryPointAccessors.fromApplication(this, AppEntryPoint::class.java)
|
|
runCatching {
|
|
entryPoint.taskNotifier().refreshChannel()
|
|
entryPoint.channelRefreshers().forEach { it.refreshChannel() }
|
|
}
|
|
}
|
|
|
|
@EntryPoint
|
|
@InstallIn(SingletonComponent::class)
|
|
interface AppEntryPoint {
|
|
fun reminderScheduler(): ReminderScheduler
|
|
fun startupGate(): StartupGate
|
|
fun providerResolver(): ProviderResolver
|
|
|
|
@ApplicationScope
|
|
fun applicationScope(): CoroutineScope
|
|
fun databaseCheckpoint(): DatabaseCheckpoint
|
|
fun taskNotifier(): TaskNotifier
|
|
fun channelRefreshers(): Set<@JvmSuppressWildcards ChannelRefresher>
|
|
fun taskWidgetUpdater(): TaskWidgetUpdater
|
|
fun providerSnapshotSource(): ProviderSnapshotSource
|
|
}
|
|
}
|