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.paging:paging-runtime-ktx:2.1.2'
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
implementation "androidx.datastore:datastore-preferences:1.0.0"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||
implementation ("com.squareup.okhttp3:okhttp-tls:$okhttp_version") {
|
||||
|
|
|
@ -2,22 +2,30 @@ package be.digitalia.fosdem
|
|||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.multidex.MultiDex
|
||||
import be.digitalia.fosdem.alarms.AppAlarmManager
|
||||
import be.digitalia.fosdem.utils.ThemeManager
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
@HiltAndroidApp
|
||||
class FosdemApplication : Application() {
|
||||
|
||||
// Injected for automatic initialization on app startup
|
||||
|
||||
@Inject
|
||||
lateinit var themeManager: ThemeManager
|
||||
|
||||
@Inject
|
||||
lateinit var alarmManager: AppAlarmManager
|
||||
|
||||
// Preload UI State SharedPreferences for faster initial access
|
||||
@Inject
|
||||
@Named("UIState")
|
||||
lateinit var preferences: SharedPreferences
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
if (BuildConfig.DEBUG) {
|
||||
|
|
|
@ -2,8 +2,8 @@ package be.digitalia.fosdem.activities
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.net.Uri
|
||||
import android.nfc.NdefRecord
|
||||
|
@ -47,7 +47,11 @@ import com.google.android.material.progressindicator.BaseProgressIndicator
|
|||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
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.Named
|
||||
|
||||
/**
|
||||
* 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 navigationView: NavigationView)
|
||||
|
||||
@Inject
|
||||
@Named("UIState")
|
||||
lateinit var preferences: SharedPreferences
|
||||
|
||||
@Inject
|
||||
lateinit var api: FosdemApi
|
||||
|
||||
@Inject
|
||||
lateinit var scheduleDao: ScheduleDao
|
||||
|
||||
|
@ -177,12 +186,13 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
|
|||
|
||||
// Latest update date, below the list
|
||||
val latestUpdateTextView: TextView = navigationView.findViewById(R.id.latest_update)
|
||||
scheduleDao.latestUpdateTime
|
||||
.observe(this) { time ->
|
||||
val timeString = if (time == -1L) getString(R.string.never)
|
||||
else DateFormat.format(LATEST_UPDATE_DATE_FORMAT, time)
|
||||
lifecycleScope.launch {
|
||||
scheduleDao.latestUpdateTime.collect { time ->
|
||||
val timeString = time?.let { DateFormat.format(LATEST_UPDATE_DATE_FORMAT, it) }
|
||||
?: getString(R.string.never)
|
||||
latestUpdateTextView.text = getString(R.string.last_update, timeString)
|
||||
}
|
||||
}
|
||||
|
||||
holder = ViewHolder(contentView, drawerLayout, navigationView)
|
||||
|
||||
|
@ -240,20 +250,21 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
|
|||
super.onStart()
|
||||
|
||||
// Scheduled database update
|
||||
lifecycleScope.launch {
|
||||
val now = System.currentTimeMillis()
|
||||
val latestUpdateTime = scheduleDao.latestUpdateTime.value
|
||||
if (latestUpdateTime == null || latestUpdateTime < now - DATABASE_VALIDITY_DURATION) {
|
||||
val prefs = getPreferences(Context.MODE_PRIVATE)
|
||||
val latestAttemptTime = prefs.getLong(PREF_LATEST_AUTO_UPDATE_ATTEMPT_TIME, -1L)
|
||||
val latestUpdateTime = scheduleDao.latestUpdateTime.first()
|
||||
if (latestUpdateTime == null || latestUpdateTime.time < now - DATABASE_VALIDITY_DURATION) {
|
||||
val latestAttemptTime = preferences.getLong(LATEST_UPDATE_ATTEMPT_TIME_PREF_KEY, -1L)
|
||||
if (latestAttemptTime == -1L || latestAttemptTime < now - AUTO_UPDATE_SNOOZE_DURATION) {
|
||||
prefs.edit {
|
||||
putLong(PREF_LATEST_AUTO_UPDATE_ATTEMPT_TIME, now)
|
||||
preferences.edit {
|
||||
putLong(LATEST_UPDATE_ATTEMPT_TIME_PREF_KEY, now)
|
||||
}
|
||||
// Try to update immediately. If it fails, the user gets a message and a retry button.
|
||||
api.downloadSchedule()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
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 DATABASE_VALIDITY_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"
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@ import android.text.format.DateUtils
|
|||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.switchMap
|
||||
import be.digitalia.fosdem.alarms.AppAlarmManager
|
||||
|
@ -22,6 +24,7 @@ import be.digitalia.fosdem.utils.network.HttpClient
|
|||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import okio.buffer
|
||||
import javax.inject.Inject
|
||||
|
@ -58,11 +61,10 @@ class FosdemApi @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private suspend fun downloadScheduleInternal() {
|
||||
_downloadScheduleState.value = LoadingState.Loading()
|
||||
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 source = if (length > 0L) {
|
||||
// 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) {
|
||||
// The room statuses will only be loaded when the event is live.
|
||||
// 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)
|
||||
var index = 0
|
||||
for (day in days) {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
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.DatabaseConfiguration
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
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.Person
|
||||
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(
|
||||
entities = [EventEntity::class, EventTitles::class, Person::class, EventToPerson::class,
|
||||
|
@ -31,24 +26,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||
abstract val scheduleDao: ScheduleDao
|
||||
abstract val bookmarksDao: BookmarksDao
|
||||
|
||||
lateinit var sharedPreferences: SharedPreferences
|
||||
private set
|
||||
// Manually injected fields, used by Daos
|
||||
lateinit var dataStore: DataStore<Preferences>
|
||||
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
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
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.MutableLiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.paging.DataSource
|
||||
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.StatusEvent
|
||||
import be.digitalia.fosdem.model.Track
|
||||
import be.digitalia.fosdem.utils.BackgroundWorkScope
|
||||
import be.digitalia.fosdem.utils.DateUtils
|
||||
import kotlinx.coroutines.async
|
||||
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
|
||||
|
||||
@Dao
|
||||
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.
|
||||
* This LiveData is pre-initialized with the up-to-date value.
|
||||
* @return The latest update time, or null if not available.
|
||||
*/
|
||||
val latestUpdateTime: LiveData<Long>
|
||||
@MainThread
|
||||
get() {
|
||||
if (_latestUpdateTime.value == null) {
|
||||
_latestUpdateTime.value = appDatabase.sharedPreferences.getLong(LAST_UPDATE_TIME_PREF, -1L)
|
||||
}
|
||||
return _latestUpdateTime
|
||||
val latestUpdateTime: Flow<Date?> = appDatabase.dataStore.data.map { prefs ->
|
||||
prefs[LATEST_UPDATE_TIME_PREF_KEY]?.let { Date(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The time identifier of the current version of the database.
|
||||
*/
|
||||
val lastModifiedTag: String?
|
||||
get() = appDatabase.sharedPreferences.getString(LAST_MODIFIED_TAG_PREF, null)
|
||||
val lastModifiedTag: Flow<String?> = appDatabase.dataStore.data.map { prefs ->
|
||||
prefs[LAST_MODIFIED_TAG_PREF]
|
||||
}
|
||||
|
||||
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
|
||||
val now = System.currentTimeMillis()
|
||||
appDatabase.sharedPreferences.edit {
|
||||
putLong(LAST_UPDATE_TIME_PREF, now)
|
||||
putString(LAST_MODIFIED_TAG_PREF, lastModifiedTag)
|
||||
runBlocking {
|
||||
appDatabase.dataStore.edit { prefs ->
|
||||
prefs.clear()
|
||||
prefs[LATEST_UPDATE_TIME_PREF_KEY] = now
|
||||
if (lastModifiedTag != null) {
|
||||
prefs[LAST_MODIFIED_TAG_PREF] = lastModifiedTag
|
||||
}
|
||||
}
|
||||
}
|
||||
_latestUpdateTime.postValue(now)
|
||||
}
|
||||
return totalEvents
|
||||
}
|
||||
|
@ -213,38 +218,30 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
|||
protected abstract fun clearDays()
|
||||
|
||||
// Cache days
|
||||
private val daysLiveDataDelegate = lazy { getDaysInternal() }
|
||||
|
||||
val days: LiveData<List<Day>> by daysLiveDataDelegate
|
||||
val days: Flow<List<Day>> by lazy {
|
||||
getDaysInternal().shareIn(
|
||||
scope = BackgroundWorkScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
replay = 1
|
||||
)
|
||||
}
|
||||
|
||||
@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
|
||||
fun getYear(): Int {
|
||||
var date = 0L
|
||||
|
||||
// Compute from cached days if available
|
||||
val days = if (daysLiveDataDelegate.isInitialized()) days.value else null
|
||||
if (days != null) {
|
||||
if (days.isNotEmpty()) {
|
||||
date = days[0].date.time
|
||||
}
|
||||
suspend fun getYear(): Int {
|
||||
// Compute from days if available
|
||||
val days = days.first()
|
||||
val date = if (days.isNotEmpty()) {
|
||||
days[0].date.time
|
||||
} else {
|
||||
date = getConferenceStartDate()
|
||||
}
|
||||
|
||||
// Use the current year by default
|
||||
if (date == 0L) {
|
||||
date = System.currentTimeMillis()
|
||||
System.currentTimeMillis()
|
||||
}
|
||||
|
||||
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
|
||||
JOIN events e ON t.id = e.track_id
|
||||
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>
|
||||
|
||||
companion object {
|
||||
private const val LAST_UPDATE_TIME_PREF = "last_update_time"
|
||||
private const val LAST_MODIFIED_TAG_PREF = "last_modified_tag"
|
||||
private val LATEST_UPDATE_TIME_PREF_KEY = longPreferencesKey("latest_update_time")
|
||||
private val LAST_MODIFIED_TAG_PREF = stringPreferencesKey("last_modified_tag")
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package be.digitalia.fosdem.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.nfc.NdefRecord
|
||||
import android.os.Bundle
|
||||
|
@ -33,6 +33,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.util.concurrent.CancellationException
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
/**
|
||||
* Bookmarks list, optionally filterable.
|
||||
|
@ -42,8 +43,12 @@ import javax.inject.Inject
|
|||
@AndroidEntryPoint
|
||||
class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataCallback {
|
||||
|
||||
@Inject
|
||||
@Named("UIState")
|
||||
lateinit var preferences: SharedPreferences
|
||||
@Inject
|
||||
lateinit var api: FosdemApi
|
||||
|
||||
private val viewModel: BookmarksViewModel by viewModels()
|
||||
private val multiChoiceHelper: MultiChoiceHelper by lazy(LazyThreadSafetyMode.NONE) {
|
||||
MultiChoiceHelper(requireActivity() as AppCompatActivity, this, object : MultiChoiceHelper.MultiChoiceModeListener {
|
||||
|
@ -91,7 +96,7 @@ class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataC
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
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
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
|
@ -145,8 +150,8 @@ class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataC
|
|||
val upcomingOnly = !viewModel.upcomingOnly
|
||||
viewModel.upcomingOnly = upcomingOnly
|
||||
updateMenuItems()
|
||||
requireActivity().getPreferences(Context.MODE_PRIVATE).edit {
|
||||
putBoolean(PREF_UPCOMING_ONLY, upcomingOnly)
|
||||
preferences.edit {
|
||||
putBoolean(UPCOMING_ONLY_PREF_KEY, upcomingOnly)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
@ -197,6 +202,6 @@ class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataC
|
|||
}
|
||||
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import be.digitalia.fosdem.R
|
||||
import be.digitalia.fosdem.db.ScheduleDao
|
||||
import be.digitalia.fosdem.model.Day
|
||||
import be.digitalia.fosdem.utils.enforceSingleScrollDirection
|
||||
import be.digitalia.fosdem.utils.instantiate
|
||||
import be.digitalia.fosdem.utils.recyclerView
|
||||
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.TabLayoutMediator
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvider {
|
||||
|
@ -34,7 +36,10 @@ class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvi
|
|||
}
|
||||
|
||||
@Inject
|
||||
lateinit var scheduleDao: ScheduleDao
|
||||
@Named("UIState")
|
||||
lateinit var preferences: SharedPreferences
|
||||
|
||||
private val viewModel: TracksViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
@ -49,10 +54,10 @@ class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvi
|
|||
|
||||
var savedCurrentPage = if (savedInstanceState == null) {
|
||||
// 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
|
||||
|
||||
scheduleDao.days.observe(viewLifecycleOwner) { days ->
|
||||
viewModel.days.observe(viewLifecycleOwner) { days ->
|
||||
holder.run {
|
||||
daysAdapter.days = days
|
||||
|
||||
|
@ -79,10 +84,9 @@ class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvi
|
|||
if (event == Lifecycle.Event.ON_STOP) {
|
||||
// Save the current page to preferences if it has changed
|
||||
val page = holder.pager.currentItem
|
||||
val prefs = requireActivity().getPreferences(Context.MODE_PRIVATE)
|
||||
if (prefs.getInt(PREF_CURRENT_PAGE, -1) != page) {
|
||||
prefs.edit {
|
||||
putInt(PREF_CURRENT_PAGE, page)
|
||||
if (preferences.getInt(TRACKS_CURRENT_PAGE_PREF_KEY, -1) != page) {
|
||||
preferences.edit {
|
||||
putInt(TRACKS_CURRENT_PAGE_PREF_KEY, page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,6 +125,6 @@ class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvi
|
|||
}
|
||||
|
||||
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.model.Day
|
||||
import be.digitalia.fosdem.model.Track
|
||||
import be.digitalia.fosdem.viewmodels.TracksViewModel
|
||||
import be.digitalia.fosdem.viewmodels.TracksListViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
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) {
|
||||
requireArguments().getParcelable(ARG_DAY)!!
|
||||
}
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
package be.digitalia.fosdem.inject
|
||||
|
||||
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.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import be.digitalia.fosdem.alarms.AppAlarmManager
|
||||
import be.digitalia.fosdem.db.AppDatabase
|
||||
import be.digitalia.fosdem.db.BookmarksDao
|
||||
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.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
@ -26,57 +27,38 @@ import javax.inject.Singleton
|
|||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
private const val DB_FILE = "fosdem.sqlite"
|
||||
private const val DB_PREFS_FILE = "database"
|
||||
private const val DB_DATASTORE_FILE = "database"
|
||||
|
||||
@Provides
|
||||
@Named("Database")
|
||||
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
|
||||
return context.applicationContext.getSharedPreferences(DB_PREFS_FILE, Context.MODE_PRIVATE)
|
||||
fun provideDataStore(@ApplicationContext context: Context): DataStore<Preferences> {
|
||||
return PreferenceDataStoreFactory.create(
|
||||
produceFile = { context.preferencesDataStoreFile(DB_DATASTORE_FILE) }
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
|
||||
val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) = with(database) {
|
||||
// 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}")
|
||||
}
|
||||
}
|
||||
|
||||
fun provideAppDatabase(@ApplicationContext context: Context,
|
||||
@Named("Database") dataStore: DataStore<Preferences>,
|
||||
alarmManager: AppAlarmManager): AppDatabase {
|
||||
return Room.databaseBuilder(context, AppDatabase::class.java, DB_FILE)
|
||||
.addMigrations(MIGRATION_1_2)
|
||||
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
|
||||
.fallbackToDestructiveMigration()
|
||||
.addCallback(object : RoomDatabase.Callback() {
|
||||
@WorkerThread
|
||||
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
|
||||
runBlocking {
|
||||
dataStore.edit { it.clear() }
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
.also {
|
||||
// Manual dependency injection
|
||||
it.dataStore = dataStore
|
||||
it.alarmManager = alarmManager
|
||||
}
|
||||
}
|
||||
|
||||
@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.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import java.io.FileNotFoundException
|
||||
|
@ -73,7 +74,8 @@ class BookmarksExportProvider : ContentProvider() {
|
|||
when (col) {
|
||||
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 -> {
|
||||
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
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
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.model.Day
|
||||
import be.digitalia.fosdem.model.Track
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class TracksViewModel @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
|
||||
}
|
||||
}
|
||||
val days: LiveData<List<Day>> = scheduleDao.days
|
||||
.asLiveData(viewModelScope.coroutineContext)
|
||||
.distinctUntilChanged()
|
||||
}
|
Loading…
Reference in a new issue