FreshRSSApplication.kt 6.46 KB
Newer Older
1 2 3
package fr.chenry.android.freshrss

import android.app.Application
Christophe Henry's avatar
Christophe Henry committed
4
import android.app.NotificationChannel
5 6 7
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
8
import androidx.annotation.VisibleForTesting
Christophe Henry's avatar
Christophe Henry committed
9
import androidx.core.app.NotificationManagerCompat
Christophe Henry's avatar
Christophe Henry committed
10
import androidx.room.Room
11
import androidx.work.*
augier's avatar
augier committed
12
import fr.chenry.android.freshrss.store.FreshRSSPreferences
13
import fr.chenry.android.freshrss.store.Store
14
import fr.chenry.android.freshrss.store.database.FreshRSSDabatabase
Christophe Henry's avatar
Christophe Henry committed
15
import fr.chenry.android.freshrss.store.database.FreshRSSDabatabase_Migrations
16
import fr.chenry.android.freshrss.utils.*
17
import kotlinx.coroutines.*
Christophe Henry's avatar
Christophe Henry committed
18 19 20 21
import org.acra.ACRA
import org.acra.annotation.*
import org.acra.data.StringFormat
import org.acra.sender.HttpSender
22 23
import org.joda.time.*
import java.util.Properties
Christophe Henry's avatar
Christophe Henry committed
24
import java.util.TimeZone
25
import java.util.concurrent.TimeUnit
26
import kotlin.math.max
27

Christophe Henry's avatar
Christophe Henry committed
28 29 30
@AcraCore(reportFormat = StringFormat.JSON)
@AcraHttpSender(
    uri = "https://android-report.christophe-henry.dev/report",
31 32
    basicAuthLogin = "3lJP3VyigIr9nxuF",
    basicAuthPassword = "pjhEv0b0rtIM7rrp",
Christophe Henry's avatar
Christophe Henry committed
33 34 35 36 37 38 39 40
    httpMethod = HttpSender.Method.POST
)
@AcraNotification(
    resChannelName = R.string.notification_channel_errors_reports_title,
    resText = R.string.notification_crash_description,
    resTitle = R.string.notification_crash_title,
    resChannelImportance = NotificationManagerCompat.IMPORTANCE_MAX
)
41
open class FreshRSSApplication: Application(), SharedPreferences.OnSharedPreferenceChangeListener {
42
    open val preferences: FreshRSSPreferences by lazy {FreshRSSPreferences(this)}
43 44
    open val workManager by lazy {WorkManager.getInstance(this)}
    open val notificationManager: NotificationManagerCompat by lazy {NotificationManagerCompat.from(this)}
45 46
    open val db by lazy {
        Room.databaseBuilder(context, FreshRSSDabatabase::class.java, DB_NAME)
Christophe Henry's avatar
Christophe Henry committed
47 48 49
            .addMigrations(*FreshRSSDabatabase_Migrations.build())
            .build()
    }
50

Christophe Henry's avatar
Christophe Henry committed
51 52
    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)
53
        initAcra()
Christophe Henry's avatar
Christophe Henry committed
54 55
    }

56 57
    override fun onCreate() {
        super.onCreate()
Christophe Henry's avatar
Christophe Henry committed
58 59
        // Stupid hack because Android is still not able to provide the current
        // application globally even though it's an actual a singleton...
60
        appInstance = this
Christophe Henry's avatar
Christophe Henry committed
61
        notificationManager.createNotificationChannels(getNotificationChannels())
62 63 64
        preferences.apply {
            initDefaults()
            registerChangeListener(this@FreshRSSApplication)
65 66 67 68 69 70
            debugMode = runCatching {
                Properties().let {
                    it.load(baseContext.assets.open("config.properties"))
                    it.getProperty("debug")!!.toBoolean()
                }
            }.getOrDefault(false)
71 72
        }

73
        // Debug
74
        //Stetho.initializeWithDefaults(this)
75 76
    }

Christophe Henry's avatar
Christophe Henry committed
77
    override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
78 79
        /* TODO: test changing the refresh frequency updates the refresh job
            Mais là, la flemme et de toutes façons, faut release */
80 81 82
        if(key == preferences.refreshFrequencyKey) GlobalScope.launch {
            workManager.cancelAllWorkByTag(RefreshWorker.PERIODIC_WORK_TAG).await()
            if(isPeriodicRefreshEnabled()) enqueuePeriodicWork(preferences.refreshFrequency)
83
        }
84 85
    }

86
    open fun forceRefresh() {
87 88
        if(workManager.getRunningRefreshWork().isEmpty()) runBlocking {
            enqueueUniqueWork()
89
        }
90
    }
91

92 93 94 95
    open fun refresh() {
        if(workManager.getRunningRefreshWork().isEmpty()) runBlocking {
            if(isPeriodicRefreshEnabled()) enqueuePeriodicWork()
            else enqueueUniqueWork()
96 97 98
        }
    }

99
    open fun cancelOnGoingRefresh(): Unit = runBlocking {
100 101 102
        workManager.cancelAllWorkByTag(RefreshWorker.REFRESH_WORK_TAG).await()
    }.unit()

103
    open fun ensurePeriodicRequest() = GlobalScope.launch {
104 105
        if(!isPeriodicRefreshEnabled()) return@launch

106 107
        val period = Store.account.value?.lastFetchDate?.let {Period(it, DateTime.now())}
        val initialDelay = preferences.refreshFrequency - (period?.minutes?.toLong() ?: preferences.refreshFrequency)
108 109 110 111 112

        if(
            workManager.getPeriodicRefreshWorks().isEmpty() ||
            initialDelay <= 0 && workManager.getPeriodicRefreshWorks().isNotEmpty()
        ) enqueuePeriodicWork(max(0, initialDelay))
113
    }
114

115 116 117 118 119
    // Allows to disengage ACRA in tests
    @VisibleForTesting
    open fun initAcra() = ACRA.init(this)

    @VisibleForTesting
120 121 122
    fun isPeriodicRefreshEnabled() = preferences.refreshFrequency > 0

    private suspend fun enqueuePeriodicWork(initialDelay: Long = 0, initialDelayTimeUnit: TimeUnit = TimeUnit.MINUTES) =
123 124 125
        workManager.enqueueUniquePeriodicWork(
            RefreshWorker.PERIODIC_WORK_NAME,
            ExistingPeriodicWorkPolicy.REPLACE,
126 127 128 129 130 131
            RefreshWorker.periodicWorkRequest(
                preferences.refreshFrequency,
                TimeUnit.MINUTES,
                initialDelay,
                initialDelayTimeUnit
            )
132
        ).await()
133 134 135 136 137 138

    private suspend fun enqueueUniqueWork() = workManager.enqueueUniqueWork(
        RefreshWorker.ONE_TIME_WORK_NAME,
        ExistingWorkPolicy.REPLACE,
        RefreshWorker.manualWorkRequest
    ).await()
139

Christophe Henry's avatar
Christophe Henry committed
140
    companion object {
141

142
        const val DB_NAME = "freshrss.db"
143
        private lateinit var appInstance: FreshRSSApplication
Christophe Henry's avatar
Christophe Henry committed
144
        val app: FreshRSSApplication get() = appInstance
Christophe Henry's avatar
Christophe Henry committed
145 146

        val db: FreshRSSDabatabase
147
            inline get() = app.db
Christophe Henry's avatar
Christophe Henry committed
148 149 150 151 152 153 154 155 156

        val context: Context
            inline get() = app.applicationContext

        val preferences: FreshRSSPreferences
            inline get() = app.preferences

        val userTimeZone: DateTimeZone
            inline get() = DateTimeZone.forTimeZone(TimeZone.getDefault())
157

Christophe Henry's avatar
Refacto  
Christophe Henry committed
158
        fun getString(id: Int, vararg formatArgs: Any = arrayOf()): String = if(formatArgs.isEmpty())
Christophe Henry's avatar
Christophe Henry committed
159 160
            app.resources.getString(id) else
            app.resources.getString(id, *formatArgs)
Christophe Henry's avatar
Christophe Henry committed
161 162 163 164 165 166 167 168 169 170 171

        private fun getNotificationChannels(): List<NotificationChannel> {
            if(Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return listOf()

            return NotificationChannels.values().map {
                NotificationChannel(it.name, getString(it.nameResourceId), it.importance.asAndroid()).apply {
                    description = getString(it.descriptionResourceId)
                    setSound(null, null)
                }
            }
        }
172
    }
Christophe Henry's avatar
Christophe Henry committed
173
}
Christophe Henry's avatar
Refacto  
Christophe Henry committed
174 175

val F = FreshRSSApplication.Companion