mirror of
https://github.com/MatomoCamp/matomocamp-companion-android.git
synced 2024-09-19 16:13:46 +02:00
Refactor UI State SharedPreferences and add Datastore to the database (#73)
- change database to use destructive migration and properly clear the extra data on each migration - store database extra data using the datastore-preferences library instead of SharedPreferences - move ScheduleDao access from TracksFragment to TracksViewModel and rename previous TracksViewModel to TracksListViewModel - centralize all UI State preferences in the same SharedPreferences file and inject it. It is loaded asynchronously on application startup to minimize blocking the main thread - change days cache in ScheduleDao from LiveData to a hot Flow.
This commit is contained in:
parent
6df1aa68b9
commit
a9184e62aa
14 changed files with 208 additions and 175 deletions
|
@ -99,6 +99,7 @@ dependencies {
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||||
implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
|
implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
|
||||||
implementation "androidx.room:room-ktx:$room_version"
|
implementation "androidx.room:room-ktx:$room_version"
|
||||||
|
implementation "androidx.datastore:datastore-preferences:1.0.0"
|
||||||
kapt "androidx.room:room-compiler:$room_version"
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||||
implementation ("com.squareup.okhttp3:okhttp-tls:$okhttp_version") {
|
implementation ("com.squareup.okhttp3:okhttp-tls:$okhttp_version") {
|
||||||
|
|
|
@ -2,22 +2,30 @@ package be.digitalia.fosdem
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import androidx.multidex.MultiDex
|
import androidx.multidex.MultiDex
|
||||||
import be.digitalia.fosdem.alarms.AppAlarmManager
|
import be.digitalia.fosdem.alarms.AppAlarmManager
|
||||||
import be.digitalia.fosdem.utils.ThemeManager
|
import be.digitalia.fosdem.utils.ThemeManager
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class FosdemApplication : Application() {
|
class FosdemApplication : Application() {
|
||||||
|
|
||||||
// Injected for automatic initialization on app startup
|
// Injected for automatic initialization on app startup
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var themeManager: ThemeManager
|
lateinit var themeManager: ThemeManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var alarmManager: AppAlarmManager
|
lateinit var alarmManager: AppAlarmManager
|
||||||
|
|
||||||
|
// Preload UI State SharedPreferences for faster initial access
|
||||||
|
@Inject
|
||||||
|
@Named("UIState")
|
||||||
|
lateinit var preferences: SharedPreferences
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context?) {
|
override fun attachBaseContext(base: Context?) {
|
||||||
super.attachBaseContext(base)
|
super.attachBaseContext(base)
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
|
|
|
@ -2,8 +2,8 @@ package be.digitalia.fosdem.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.nfc.NdefRecord
|
import android.nfc.NdefRecord
|
||||||
|
@ -47,7 +47,11 @@ import com.google.android.material.progressindicator.BaseProgressIndicator
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main entry point of the application. Allows to switch between section fragments and update the database.
|
* Main entry point of the application. Allows to switch between section fragments and update the database.
|
||||||
|
@ -78,8 +82,13 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
|
||||||
val drawerLayout: DrawerLayout,
|
val drawerLayout: DrawerLayout,
|
||||||
val navigationView: NavigationView)
|
val navigationView: NavigationView)
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@Named("UIState")
|
||||||
|
lateinit var preferences: SharedPreferences
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var api: FosdemApi
|
lateinit var api: FosdemApi
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var scheduleDao: ScheduleDao
|
lateinit var scheduleDao: ScheduleDao
|
||||||
|
|
||||||
|
@ -177,12 +186,13 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
|
||||||
|
|
||||||
// Latest update date, below the list
|
// Latest update date, below the list
|
||||||
val latestUpdateTextView: TextView = navigationView.findViewById(R.id.latest_update)
|
val latestUpdateTextView: TextView = navigationView.findViewById(R.id.latest_update)
|
||||||
scheduleDao.latestUpdateTime
|
lifecycleScope.launch {
|
||||||
.observe(this) { time ->
|
scheduleDao.latestUpdateTime.collect { time ->
|
||||||
val timeString = if (time == -1L) getString(R.string.never)
|
val timeString = time?.let { DateFormat.format(LATEST_UPDATE_DATE_FORMAT, it) }
|
||||||
else DateFormat.format(LATEST_UPDATE_DATE_FORMAT, time)
|
?: getString(R.string.never)
|
||||||
latestUpdateTextView.text = getString(R.string.last_update, timeString)
|
latestUpdateTextView.text = getString(R.string.last_update, timeString)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
holder = ViewHolder(contentView, drawerLayout, navigationView)
|
holder = ViewHolder(contentView, drawerLayout, navigationView)
|
||||||
|
|
||||||
|
@ -240,20 +250,21 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
|
||||||
super.onStart()
|
super.onStart()
|
||||||
|
|
||||||
// Scheduled database update
|
// Scheduled database update
|
||||||
|
lifecycleScope.launch {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val latestUpdateTime = scheduleDao.latestUpdateTime.value
|
val latestUpdateTime = scheduleDao.latestUpdateTime.first()
|
||||||
if (latestUpdateTime == null || latestUpdateTime < now - DATABASE_VALIDITY_DURATION) {
|
if (latestUpdateTime == null || latestUpdateTime.time < now - DATABASE_VALIDITY_DURATION) {
|
||||||
val prefs = getPreferences(Context.MODE_PRIVATE)
|
val latestAttemptTime = preferences.getLong(LATEST_UPDATE_ATTEMPT_TIME_PREF_KEY, -1L)
|
||||||
val latestAttemptTime = prefs.getLong(PREF_LATEST_AUTO_UPDATE_ATTEMPT_TIME, -1L)
|
|
||||||
if (latestAttemptTime == -1L || latestAttemptTime < now - AUTO_UPDATE_SNOOZE_DURATION) {
|
if (latestAttemptTime == -1L || latestAttemptTime < now - AUTO_UPDATE_SNOOZE_DURATION) {
|
||||||
prefs.edit {
|
preferences.edit {
|
||||||
putLong(PREF_LATEST_AUTO_UPDATE_ATTEMPT_TIME, now)
|
putLong(LATEST_UPDATE_ATTEMPT_TIME_PREF_KEY, now)
|
||||||
}
|
}
|
||||||
// Try to update immediately. If it fails, the user gets a message and a retry button.
|
// Try to update immediately. If it fails, the user gets a message and a retry button.
|
||||||
api.downloadSchedule()
|
api.downloadSchedule()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.main, menu)
|
menuInflater.inflate(R.menu.main, menu)
|
||||||
|
@ -349,7 +360,7 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
|
||||||
private const val ERROR_MESSAGE_DISPLAY_DURATION = 5000
|
private const val ERROR_MESSAGE_DISPLAY_DURATION = 5000
|
||||||
private const val DATABASE_VALIDITY_DURATION = DateUtils.DAY_IN_MILLIS
|
private const val DATABASE_VALIDITY_DURATION = DateUtils.DAY_IN_MILLIS
|
||||||
private const val AUTO_UPDATE_SNOOZE_DURATION = DateUtils.DAY_IN_MILLIS
|
private const val AUTO_UPDATE_SNOOZE_DURATION = DateUtils.DAY_IN_MILLIS
|
||||||
private const val PREF_LATEST_AUTO_UPDATE_ATTEMPT_TIME = "last_download_reminder_time"
|
private const val LATEST_UPDATE_ATTEMPT_TIME_PREF_KEY = "latest_update_attempt_time"
|
||||||
private const val LATEST_UPDATE_DATE_FORMAT = "d MMM yyyy kk:mm:ss"
|
private const val LATEST_UPDATE_DATE_FORMAT = "d MMM yyyy kk:mm:ss"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,6 +5,8 @@ import android.text.format.DateUtils
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
|
import androidx.lifecycle.distinctUntilChanged
|
||||||
import androidx.lifecycle.liveData
|
import androidx.lifecycle.liveData
|
||||||
import androidx.lifecycle.switchMap
|
import androidx.lifecycle.switchMap
|
||||||
import be.digitalia.fosdem.alarms.AppAlarmManager
|
import be.digitalia.fosdem.alarms.AppAlarmManager
|
||||||
|
@ -22,6 +24,7 @@ import be.digitalia.fosdem.utils.network.HttpClient
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -58,11 +61,10 @@ class FosdemApi @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainThread
|
|
||||||
private suspend fun downloadScheduleInternal() {
|
private suspend fun downloadScheduleInternal() {
|
||||||
_downloadScheduleState.value = LoadingState.Loading()
|
_downloadScheduleState.value = LoadingState.Loading()
|
||||||
val res = try {
|
val res = try {
|
||||||
val response = httpClient.get(FosdemUrls.schedule, scheduleDao.lastModifiedTag) { body, headers ->
|
val response = httpClient.get(FosdemUrls.schedule, scheduleDao.lastModifiedTag.first()) { body, headers ->
|
||||||
val length = body.contentLength()
|
val length = body.contentLength()
|
||||||
val source = if (length > 0L) {
|
val source = if (length > 0L) {
|
||||||
// Broadcast the progression in percents, with a precision of 1/10 of the total file size
|
// Broadcast the progression in percents, with a precision of 1/10 of the total file size
|
||||||
|
@ -97,7 +99,7 @@ class FosdemApi @Inject constructor(
|
||||||
val roomStatuses: LiveData<Map<String, RoomStatus>> by lazy(LazyThreadSafetyMode.NONE) {
|
val roomStatuses: LiveData<Map<String, RoomStatus>> by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
// The room statuses will only be loaded when the event is live.
|
// The room statuses will only be loaded when the event is live.
|
||||||
// Use the days from the database to determine it.
|
// Use the days from the database to determine it.
|
||||||
val scheduler = scheduleDao.days.switchMap { days ->
|
val scheduler = scheduleDao.days.asLiveData().distinctUntilChanged().switchMap { days ->
|
||||||
val startEndTimestamps = LongArray(days.size * 2)
|
val startEndTimestamps = LongArray(days.size * 2)
|
||||||
var index = 0
|
var index = 0
|
||||||
for (day in days) {
|
for (day in days) {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package be.digitalia.fosdem.db
|
package be.digitalia.fosdem.db
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.DatabaseConfiguration
|
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import be.digitalia.fosdem.alarms.AppAlarmManager
|
import be.digitalia.fosdem.alarms.AppAlarmManager
|
||||||
|
@ -15,11 +15,6 @@ import be.digitalia.fosdem.model.Day
|
||||||
import be.digitalia.fosdem.model.Link
|
import be.digitalia.fosdem.model.Link
|
||||||
import be.digitalia.fosdem.model.Person
|
import be.digitalia.fosdem.model.Person
|
||||||
import be.digitalia.fosdem.model.Track
|
import be.digitalia.fosdem.model.Track
|
||||||
import dagger.hilt.EntryPoint
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.android.EntryPointAccessors
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import javax.inject.Named
|
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [EventEntity::class, EventTitles::class, Person::class, EventToPerson::class,
|
entities = [EventEntity::class, EventTitles::class, Person::class, EventToPerson::class,
|
||||||
|
@ -31,24 +26,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract val scheduleDao: ScheduleDao
|
abstract val scheduleDao: ScheduleDao
|
||||||
abstract val bookmarksDao: BookmarksDao
|
abstract val bookmarksDao: BookmarksDao
|
||||||
|
|
||||||
lateinit var sharedPreferences: SharedPreferences
|
// Manually injected fields, used by Daos
|
||||||
private set
|
lateinit var dataStore: DataStore<Preferences>
|
||||||
lateinit var alarmManager: AppAlarmManager
|
lateinit var alarmManager: AppAlarmManager
|
||||||
private set
|
|
||||||
|
|
||||||
override fun init(configuration: DatabaseConfiguration) {
|
|
||||||
super.init(configuration)
|
|
||||||
// Manual dependency injection
|
|
||||||
val entryPoint = EntryPointAccessors.fromApplication(configuration.context, AppDatabaseEntryPoint::class.java)
|
|
||||||
sharedPreferences = entryPoint.sharedPreferences
|
|
||||||
alarmManager = entryPoint.alarmManager
|
|
||||||
}
|
|
||||||
|
|
||||||
@EntryPoint
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
interface AppDatabaseEntryPoint {
|
|
||||||
@get:Named("Database")
|
|
||||||
val sharedPreferences: SharedPreferences
|
|
||||||
val alarmManager: AppAlarmManager
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
package be.digitalia.fosdem.db
|
package be.digitalia.fosdem.db
|
||||||
|
|
||||||
import androidx.annotation.MainThread
|
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.core.content.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.longPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.liveData
|
import androidx.lifecycle.liveData
|
||||||
import androidx.paging.DataSource
|
import androidx.paging.DataSource
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
|
@ -23,34 +23,35 @@ import be.digitalia.fosdem.model.Link
|
||||||
import be.digitalia.fosdem.model.Person
|
import be.digitalia.fosdem.model.Person
|
||||||
import be.digitalia.fosdem.model.StatusEvent
|
import be.digitalia.fosdem.model.StatusEvent
|
||||||
import be.digitalia.fosdem.model.Track
|
import be.digitalia.fosdem.model.Track
|
||||||
|
import be.digitalia.fosdem.utils.BackgroundWorkScope
|
||||||
import be.digitalia.fosdem.utils.DateUtils
|
import be.digitalia.fosdem.utils.DateUtils
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.shareIn
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.util.Date
|
||||||
import java.util.HashSet
|
import java.util.HashSet
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
||||||
|
|
||||||
private val _latestUpdateTime = MutableLiveData<Long>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The last update time in milliseconds since EPOCH, or -1 if not available.
|
* @return The latest update time, or null if not available.
|
||||||
* This LiveData is pre-initialized with the up-to-date value.
|
|
||||||
*/
|
*/
|
||||||
val latestUpdateTime: LiveData<Long>
|
val latestUpdateTime: Flow<Date?> = appDatabase.dataStore.data.map { prefs ->
|
||||||
@MainThread
|
prefs[LATEST_UPDATE_TIME_PREF_KEY]?.let { Date(it) }
|
||||||
get() {
|
|
||||||
if (_latestUpdateTime.value == null) {
|
|
||||||
_latestUpdateTime.value = appDatabase.sharedPreferences.getLong(LAST_UPDATE_TIME_PREF, -1L)
|
|
||||||
}
|
|
||||||
return _latestUpdateTime
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The time identifier of the current version of the database.
|
* @return The time identifier of the current version of the database.
|
||||||
*/
|
*/
|
||||||
val lastModifiedTag: String?
|
val lastModifiedTag: Flow<String?> = appDatabase.dataStore.data.map { prefs ->
|
||||||
get() = appDatabase.sharedPreferences.getString(LAST_MODIFIED_TAG_PREF, null)
|
prefs[LAST_MODIFIED_TAG_PREF]
|
||||||
|
}
|
||||||
|
|
||||||
private class EmptyScheduleException : Exception()
|
private class EmptyScheduleException : Exception()
|
||||||
|
|
||||||
|
@ -69,11 +70,15 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
||||||
}
|
}
|
||||||
if (totalEvents > 0) { // Set last update time and server's last modified tag
|
if (totalEvents > 0) { // Set last update time and server's last modified tag
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
appDatabase.sharedPreferences.edit {
|
runBlocking {
|
||||||
putLong(LAST_UPDATE_TIME_PREF, now)
|
appDatabase.dataStore.edit { prefs ->
|
||||||
putString(LAST_MODIFIED_TAG_PREF, lastModifiedTag)
|
prefs.clear()
|
||||||
|
prefs[LATEST_UPDATE_TIME_PREF_KEY] = now
|
||||||
|
if (lastModifiedTag != null) {
|
||||||
|
prefs[LAST_MODIFIED_TAG_PREF] = lastModifiedTag
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_latestUpdateTime.postValue(now)
|
|
||||||
}
|
}
|
||||||
return totalEvents
|
return totalEvents
|
||||||
}
|
}
|
||||||
|
@ -213,38 +218,30 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
||||||
protected abstract fun clearDays()
|
protected abstract fun clearDays()
|
||||||
|
|
||||||
// Cache days
|
// Cache days
|
||||||
private val daysLiveDataDelegate = lazy { getDaysInternal() }
|
val days: Flow<List<Day>> by lazy {
|
||||||
|
getDaysInternal().shareIn(
|
||||||
val days: LiveData<List<Day>> by daysLiveDataDelegate
|
scope = BackgroundWorkScope,
|
||||||
|
started = SharingStarted.Eagerly,
|
||||||
|
replay = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Query("SELECT `index`, date FROM days ORDER BY `index` ASC")
|
@Query("SELECT `index`, date FROM days ORDER BY `index` ASC")
|
||||||
protected abstract fun getDaysInternal(): LiveData<List<Day>>
|
protected abstract fun getDaysInternal(): Flow<List<Day>>
|
||||||
|
|
||||||
@WorkerThread
|
suspend fun getYear(): Int {
|
||||||
fun getYear(): Int {
|
// Compute from days if available
|
||||||
var date = 0L
|
val days = days.first()
|
||||||
|
val date = if (days.isNotEmpty()) {
|
||||||
// Compute from cached days if available
|
days[0].date.time
|
||||||
val days = if (daysLiveDataDelegate.isInitialized()) days.value else null
|
|
||||||
if (days != null) {
|
|
||||||
if (days.isNotEmpty()) {
|
|
||||||
date = days[0].date.time
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
date = getConferenceStartDate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the current year by default
|
// Use the current year by default
|
||||||
if (date == 0L) {
|
System.currentTimeMillis()
|
||||||
date = System.currentTimeMillis()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return DateUtils.getYear(date)
|
return DateUtils.getYear(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query("SELECT date FROM days ORDER BY `index` ASC LIMIT 1")
|
|
||||||
protected abstract fun getConferenceStartDate(): Long
|
|
||||||
|
|
||||||
@Query("""SELECT t.id, t.name, t.type FROM tracks t
|
@Query("""SELECT t.id, t.name, t.type FROM tracks t
|
||||||
JOIN events e ON t.id = e.track_id
|
JOIN events e ON t.id = e.track_id
|
||||||
WHERE e.day_index = :day
|
WHERE e.day_index = :day
|
||||||
|
@ -436,7 +433,7 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
||||||
protected abstract suspend fun getLinks(event: Event?): List<Link>
|
protected abstract suspend fun getLinks(event: Event?): List<Link>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val LAST_UPDATE_TIME_PREF = "last_update_time"
|
private val LATEST_UPDATE_TIME_PREF_KEY = longPreferencesKey("latest_update_time")
|
||||||
private const val LAST_MODIFIED_TAG_PREF = "last_modified_tag"
|
private val LAST_MODIFIED_TAG_PREF = stringPreferencesKey("last_modified_tag")
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
package be.digitalia.fosdem.fragments
|
package be.digitalia.fosdem.fragments
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.nfc.NdefRecord
|
import android.nfc.NdefRecord
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -33,6 +33,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.util.concurrent.CancellationException
|
import java.util.concurrent.CancellationException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bookmarks list, optionally filterable.
|
* Bookmarks list, optionally filterable.
|
||||||
|
@ -42,8 +43,12 @@ import javax.inject.Inject
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataCallback {
|
class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataCallback {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@Named("UIState")
|
||||||
|
lateinit var preferences: SharedPreferences
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var api: FosdemApi
|
lateinit var api: FosdemApi
|
||||||
|
|
||||||
private val viewModel: BookmarksViewModel by viewModels()
|
private val viewModel: BookmarksViewModel by viewModels()
|
||||||
private val multiChoiceHelper: MultiChoiceHelper by lazy(LazyThreadSafetyMode.NONE) {
|
private val multiChoiceHelper: MultiChoiceHelper by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
MultiChoiceHelper(requireActivity() as AppCompatActivity, this, object : MultiChoiceHelper.MultiChoiceModeListener {
|
MultiChoiceHelper(requireActivity() as AppCompatActivity, this, object : MultiChoiceHelper.MultiChoiceModeListener {
|
||||||
|
@ -91,7 +96,7 @@ class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataC
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val upcomingOnly = requireActivity().getPreferences(Context.MODE_PRIVATE).getBoolean(PREF_UPCOMING_ONLY, false)
|
val upcomingOnly = preferences.getBoolean(UPCOMING_ONLY_PREF_KEY, false)
|
||||||
viewModel.upcomingOnly = upcomingOnly
|
viewModel.upcomingOnly = upcomingOnly
|
||||||
|
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
|
@ -145,8 +150,8 @@ class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataC
|
||||||
val upcomingOnly = !viewModel.upcomingOnly
|
val upcomingOnly = !viewModel.upcomingOnly
|
||||||
viewModel.upcomingOnly = upcomingOnly
|
viewModel.upcomingOnly = upcomingOnly
|
||||||
updateMenuItems()
|
updateMenuItems()
|
||||||
requireActivity().getPreferences(Context.MODE_PRIVATE).edit {
|
preferences.edit {
|
||||||
putBoolean(PREF_UPCOMING_ONLY, upcomingOnly)
|
putBoolean(UPCOMING_ONLY_PREF_KEY, upcomingOnly)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -197,6 +202,6 @@ class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataC
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val PREF_UPCOMING_ONLY = "bookmarks_upcoming_only"
|
private const val UPCOMING_ONLY_PREF_KEY = "bookmarks_upcoming_only"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,27 +1,29 @@
|
||||||
package be.digitalia.fosdem.fragments
|
package be.digitalia.fosdem.fragments
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import be.digitalia.fosdem.R
|
import be.digitalia.fosdem.R
|
||||||
import be.digitalia.fosdem.db.ScheduleDao
|
|
||||||
import be.digitalia.fosdem.model.Day
|
import be.digitalia.fosdem.model.Day
|
||||||
import be.digitalia.fosdem.utils.enforceSingleScrollDirection
|
import be.digitalia.fosdem.utils.enforceSingleScrollDirection
|
||||||
import be.digitalia.fosdem.utils.instantiate
|
import be.digitalia.fosdem.utils.instantiate
|
||||||
import be.digitalia.fosdem.utils.recyclerView
|
import be.digitalia.fosdem.utils.recyclerView
|
||||||
import be.digitalia.fosdem.utils.viewLifecycleLazy
|
import be.digitalia.fosdem.utils.viewLifecycleLazy
|
||||||
|
import be.digitalia.fosdem.viewmodels.TracksViewModel
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvider {
|
class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvider {
|
||||||
|
@ -34,7 +36,10 @@ class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvi
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var scheduleDao: ScheduleDao
|
@Named("UIState")
|
||||||
|
lateinit var preferences: SharedPreferences
|
||||||
|
|
||||||
|
private val viewModel: TracksViewModel by viewModels()
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
@ -49,10 +54,10 @@ class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvi
|
||||||
|
|
||||||
var savedCurrentPage = if (savedInstanceState == null) {
|
var savedCurrentPage = if (savedInstanceState == null) {
|
||||||
// Restore the current page from preferences
|
// Restore the current page from preferences
|
||||||
requireActivity().getPreferences(Context.MODE_PRIVATE).getInt(PREF_CURRENT_PAGE, -1)
|
preferences.getInt(TRACKS_CURRENT_PAGE_PREF_KEY, -1)
|
||||||
} else -1
|
} else -1
|
||||||
|
|
||||||
scheduleDao.days.observe(viewLifecycleOwner) { days ->
|
viewModel.days.observe(viewLifecycleOwner) { days ->
|
||||||
holder.run {
|
holder.run {
|
||||||
daysAdapter.days = days
|
daysAdapter.days = days
|
||||||
|
|
||||||
|
@ -79,10 +84,9 @@ class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvi
|
||||||
if (event == Lifecycle.Event.ON_STOP) {
|
if (event == Lifecycle.Event.ON_STOP) {
|
||||||
// Save the current page to preferences if it has changed
|
// Save the current page to preferences if it has changed
|
||||||
val page = holder.pager.currentItem
|
val page = holder.pager.currentItem
|
||||||
val prefs = requireActivity().getPreferences(Context.MODE_PRIVATE)
|
if (preferences.getInt(TRACKS_CURRENT_PAGE_PREF_KEY, -1) != page) {
|
||||||
if (prefs.getInt(PREF_CURRENT_PAGE, -1) != page) {
|
preferences.edit {
|
||||||
prefs.edit {
|
putInt(TRACKS_CURRENT_PAGE_PREF_KEY, page)
|
||||||
putInt(PREF_CURRENT_PAGE, page)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,6 +125,6 @@ class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvi
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val PREF_CURRENT_PAGE = "tracks_current_page"
|
private const val TRACKS_CURRENT_PAGE_PREF_KEY = "tracks_current_page"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -18,13 +18,13 @@ import be.digitalia.fosdem.R
|
||||||
import be.digitalia.fosdem.activities.TrackScheduleActivity
|
import be.digitalia.fosdem.activities.TrackScheduleActivity
|
||||||
import be.digitalia.fosdem.model.Day
|
import be.digitalia.fosdem.model.Day
|
||||||
import be.digitalia.fosdem.model.Track
|
import be.digitalia.fosdem.model.Track
|
||||||
import be.digitalia.fosdem.viewmodels.TracksViewModel
|
import be.digitalia.fosdem.viewmodels.TracksListViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class TracksListFragment : Fragment(R.layout.recyclerview) {
|
class TracksListFragment : Fragment(R.layout.recyclerview) {
|
||||||
|
|
||||||
private val viewModel: TracksViewModel by viewModels()
|
private val viewModel: TracksListViewModel by viewModels()
|
||||||
private val day by lazy<Day>(LazyThreadSafetyMode.NONE) {
|
private val day by lazy<Day>(LazyThreadSafetyMode.NONE) {
|
||||||
requireArguments().getParcelable(ARG_DAY)!!
|
requireArguments().getParcelable(ARG_DAY)!!
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
package be.digitalia.fosdem.inject
|
package be.digitalia.fosdem.inject
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.migration.Migration
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import be.digitalia.fosdem.alarms.AppAlarmManager
|
||||||
import be.digitalia.fosdem.db.AppDatabase
|
import be.digitalia.fosdem.db.AppDatabase
|
||||||
import be.digitalia.fosdem.db.BookmarksDao
|
import be.digitalia.fosdem.db.BookmarksDao
|
||||||
import be.digitalia.fosdem.db.ScheduleDao
|
import be.digitalia.fosdem.db.ScheduleDao
|
||||||
import be.digitalia.fosdem.db.entities.Bookmark
|
|
||||||
import be.digitalia.fosdem.db.entities.EventEntity
|
|
||||||
import be.digitalia.fosdem.model.Day
|
|
||||||
import be.digitalia.fosdem.model.Link
|
|
||||||
import be.digitalia.fosdem.model.Track
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@ -26,57 +27,38 @@ import javax.inject.Singleton
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object DatabaseModule {
|
object DatabaseModule {
|
||||||
private const val DB_FILE = "fosdem.sqlite"
|
private const val DB_FILE = "fosdem.sqlite"
|
||||||
private const val DB_PREFS_FILE = "database"
|
private const val DB_DATASTORE_FILE = "database"
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Named("Database")
|
@Named("Database")
|
||||||
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
|
fun provideDataStore(@ApplicationContext context: Context): DataStore<Preferences> {
|
||||||
return context.applicationContext.getSharedPreferences(DB_PREFS_FILE, Context.MODE_PRIVATE)
|
return PreferenceDataStoreFactory.create(
|
||||||
|
produceFile = { context.preferencesDataStoreFile(DB_DATASTORE_FILE) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
|
fun provideAppDatabase(@ApplicationContext context: Context,
|
||||||
val MIGRATION_1_2 = object : Migration(1, 2) {
|
@Named("Database") dataStore: DataStore<Preferences>,
|
||||||
override fun migrate(database: SupportSQLiteDatabase) = with(database) {
|
alarmManager: AppAlarmManager): AppDatabase {
|
||||||
// Events: make primary key and track_id not null
|
|
||||||
execSQL("CREATE TABLE tmp_${EventEntity.TABLE_NAME} (id INTEGER PRIMARY KEY NOT NULL, day_index INTEGER NOT NULL, start_time INTEGER, end_time INTEGER, room_name TEXT, slug TEXT, track_id INTEGER NOT NULL, abstract TEXT, description TEXT)")
|
|
||||||
execSQL("INSERT INTO tmp_${EventEntity.TABLE_NAME} SELECT * FROM ${EventEntity.TABLE_NAME}")
|
|
||||||
execSQL("DROP TABLE ${EventEntity.TABLE_NAME}")
|
|
||||||
execSQL("ALTER TABLE tmp_${EventEntity.TABLE_NAME} RENAME TO ${EventEntity.TABLE_NAME}")
|
|
||||||
execSQL("CREATE INDEX event_day_index_idx ON ${EventEntity.TABLE_NAME} (day_index)")
|
|
||||||
execSQL("CREATE INDEX event_start_time_idx ON ${EventEntity.TABLE_NAME} (start_time)")
|
|
||||||
execSQL("CREATE INDEX event_end_time_idx ON ${EventEntity.TABLE_NAME} (end_time)")
|
|
||||||
execSQL("CREATE INDEX event_track_id_idx ON ${EventEntity.TABLE_NAME} (track_id)")
|
|
||||||
// Links: add explicit primary key
|
|
||||||
execSQL("CREATE TABLE tmp_${Link.TABLE_NAME} (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, event_id INTEGER NOT NULL, url TEXT NOT NULL, description TEXT)")
|
|
||||||
execSQL("INSERT INTO tmp_${Link.TABLE_NAME} SELECT `rowid` AS id, event_id, url, description FROM ${Link.TABLE_NAME}")
|
|
||||||
execSQL("DROP TABLE ${Link.TABLE_NAME}")
|
|
||||||
execSQL("ALTER TABLE tmp_${Link.TABLE_NAME} RENAME TO ${Link.TABLE_NAME}")
|
|
||||||
execSQL("CREATE INDEX link_event_id_idx ON ${Link.TABLE_NAME} (event_id)")
|
|
||||||
// Tracks: make primary key not null
|
|
||||||
execSQL("CREATE TABLE tmp_${Track.TABLE_NAME} (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, type TEXT NOT NULL)")
|
|
||||||
execSQL("INSERT INTO tmp_${Track.TABLE_NAME} SELECT * FROM ${Track.TABLE_NAME}")
|
|
||||||
execSQL("DROP TABLE ${Track.TABLE_NAME}")
|
|
||||||
execSQL("ALTER TABLE tmp_${Track.TABLE_NAME} RENAME TO ${Track.TABLE_NAME}")
|
|
||||||
execSQL("CREATE UNIQUE INDEX track_main_idx ON ${Track.TABLE_NAME} (name, type)")
|
|
||||||
// Days: make primary key not null and rename _index to index
|
|
||||||
execSQL("CREATE TABLE tmp_${Day.TABLE_NAME} (`index` INTEGER PRIMARY KEY NOT NULL, date INTEGER NOT NULL)")
|
|
||||||
execSQL("INSERT INTO tmp_${Day.TABLE_NAME} SELECT _index as `index`, date FROM ${Day.TABLE_NAME}")
|
|
||||||
execSQL("DROP TABLE ${Day.TABLE_NAME}")
|
|
||||||
execSQL("ALTER TABLE tmp_${Day.TABLE_NAME} RENAME TO ${Day.TABLE_NAME}")
|
|
||||||
// Bookmarks: make primary key not null
|
|
||||||
execSQL("CREATE TABLE tmp_${Bookmark.TABLE_NAME} (event_id INTEGER PRIMARY KEY NOT NULL)")
|
|
||||||
execSQL("INSERT INTO tmp_${Bookmark.TABLE_NAME} SELECT * FROM ${Bookmark.TABLE_NAME}")
|
|
||||||
execSQL("DROP TABLE ${Bookmark.TABLE_NAME}")
|
|
||||||
execSQL("ALTER TABLE tmp_${Bookmark.TABLE_NAME} RENAME TO ${Bookmark.TABLE_NAME}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Room.databaseBuilder(context, AppDatabase::class.java, DB_FILE)
|
return Room.databaseBuilder(context, AppDatabase::class.java, DB_FILE)
|
||||||
.addMigrations(MIGRATION_1_2)
|
|
||||||
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
|
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.addCallback(object : RoomDatabase.Callback() {
|
||||||
|
@WorkerThread
|
||||||
|
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
|
||||||
|
runBlocking {
|
||||||
|
dataStore.edit { it.clear() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.build()
|
.build()
|
||||||
|
.also {
|
||||||
|
// Manual dependency injection
|
||||||
|
it.dataStore = dataStore
|
||||||
|
it.alarmManager = alarmManager
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package be.digitalia.fosdem.inject
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Named
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object UIStateModule {
|
||||||
|
private const val SHARED_PREFERENCES_NAME = "ui_state"
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Named("UIState")
|
||||||
|
@Singleton
|
||||||
|
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
|
||||||
|
return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ import dagger.hilt.EntryPoint
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.EntryPointAccessors
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
@ -73,7 +74,8 @@ class BookmarksExportProvider : ContentProvider() {
|
||||||
when (col) {
|
when (col) {
|
||||||
OpenableColumns.DISPLAY_NAME -> {
|
OpenableColumns.DISPLAY_NAME -> {
|
||||||
cols[columnCount] = OpenableColumns.DISPLAY_NAME
|
cols[columnCount] = OpenableColumns.DISPLAY_NAME
|
||||||
values[columnCount++] = ctx.getString(R.string.export_bookmarks_file_name, scheduleDao.getYear())
|
val year = runBlocking { scheduleDao.getYear() }
|
||||||
|
values[columnCount++] = ctx.getString(R.string.export_bookmarks_file_name, year)
|
||||||
}
|
}
|
||||||
OpenableColumns.SIZE -> {
|
OpenableColumns.SIZE -> {
|
||||||
cols[columnCount] = OpenableColumns.SIZE
|
cols[columnCount] = OpenableColumns.SIZE
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package be.digitalia.fosdem.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.switchMap
|
||||||
|
import be.digitalia.fosdem.db.ScheduleDao
|
||||||
|
import be.digitalia.fosdem.model.Day
|
||||||
|
import be.digitalia.fosdem.model.Track
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class TracksListViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
||||||
|
|
||||||
|
private val dayLiveData = MutableLiveData<Day>()
|
||||||
|
|
||||||
|
val tracks: LiveData<List<Track>> = dayLiveData.switchMap { day: Day ->
|
||||||
|
scheduleDao.getTracks(day)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDay(day: Day) {
|
||||||
|
if (day != dayLiveData.value) {
|
||||||
|
dayLiveData.value = day
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,27 +1,19 @@
|
||||||
package be.digitalia.fosdem.viewmodels
|
package be.digitalia.fosdem.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.switchMap
|
import androidx.lifecycle.asLiveData
|
||||||
|
import androidx.lifecycle.distinctUntilChanged
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import be.digitalia.fosdem.db.ScheduleDao
|
import be.digitalia.fosdem.db.ScheduleDao
|
||||||
import be.digitalia.fosdem.model.Day
|
import be.digitalia.fosdem.model.Day
|
||||||
import be.digitalia.fosdem.model.Track
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class TracksViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
class TracksViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
||||||
|
|
||||||
private val dayLiveData = MutableLiveData<Day>()
|
val days: LiveData<List<Day>> = scheduleDao.days
|
||||||
|
.asLiveData(viewModelScope.coroutineContext)
|
||||||
val tracks: LiveData<List<Track>> = dayLiveData.switchMap { day: Day ->
|
.distinctUntilChanged()
|
||||||
scheduleDao.getTracks(day)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setDay(day: Day) {
|
|
||||||
if (day != dayLiveData.value) {
|
|
||||||
dayLiveData.value = day
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
Loading…
Reference in a new issue