1
0
Fork 0
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:
Christophe Beyls 2021-11-22 19:44:02 +01:00 committed by GitHub
parent 6df1aa68b9
commit a9184e62aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 208 additions and 175 deletions

View file

@ -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") {

View file

@ -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) {

View file

@ -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)
latestUpdateTextView.text = getString(R.string.last_update, timeString)
}
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,17 +250,18 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
super.onStart()
// Scheduled database update
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)
if (latestAttemptTime == -1L || latestAttemptTime < now - AUTO_UPDATE_SNOOZE_DURATION) {
prefs.edit {
putLong(PREF_LATEST_AUTO_UPDATE_ATTEMPT_TIME, now)
lifecycleScope.launch {
val now = System.currentTimeMillis()
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) {
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()
}
// Try to update immediately. If it fails, the user gets a message and a retry button.
api.downloadSchedule()
}
}
}
@ -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"
}
}

View file

@ -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) {

View file

@ -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
}
}

View file

@ -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()
// Use the current year by default
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")
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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)!!
}

View file

@ -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)
.build()
.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

View file

@ -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)
}
}

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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()
}