1
0
Fork 0
mirror of https://github.com/MatomoCamp/matomocamp-companion-android.git synced 2024-09-19 16:13:46 +02:00

Add dependency injection using Hilt (#68)

Configure Hilt to inject FosdemApi, FosdemAlarmManager, BookmarksDao and ScheduleDao
This commit is contained in:
Christophe Beyls 2021-05-14 21:47:10 +02:00 committed by GitHub
parent d5e246ef20
commit 684131fb51
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 540 additions and 332 deletions

View file

@ -32,6 +32,7 @@ The result apk file will be placed in ```app/build/outputs/apk/```.
## Used libraries
* [Android Jetpack](https://developer.android.com/jetpack) by The Android Open Source Project
* [Dagger Hilt](https://dagger.dev/hilt/) by The Dagger Authors
* [Material Components for Android](https://material.io/develop/android) by The Android Open Source Project
* [OkHttp](https://github.com/square/okhttp) by Square, Inc.
* [Moshi](https://github.com/square/moshi) by Square, Inc.

View file

@ -1,7 +1,10 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-parcelize'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
compileSdkVersion 30
@ -71,6 +74,8 @@ dependencies {
def room_version = "2.3.0"
def okhttp_version = "3.12.13"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.fragment:fragment-ktx:1.3.3'

View file

@ -4,9 +4,15 @@ import android.app.Application
import androidx.preference.PreferenceManager
import be.digitalia.fosdem.alarms.FosdemAlarmManager
import be.digitalia.fosdem.utils.ThemeManager
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
@HiltAndroidApp
class FosdemApplication : Application() {
@Inject
lateinit var alarmManager: FosdemAlarmManager
override fun onCreate() {
super.onCreate()
@ -15,7 +21,7 @@ class FosdemApplication : Application() {
// Light/Dark theme switch (requires settings)
ThemeManager.init(this)
// Alarms (requires settings)
FosdemAlarmManager.init(this)
alarmManager.init()
}
}

View file

@ -28,12 +28,14 @@ import be.digitalia.fosdem.utils.toNfcAppData
import be.digitalia.fosdem.viewmodels.BookmarkStatusViewModel
import be.digitalia.fosdem.viewmodels.EventViewModel
import be.digitalia.fosdem.widgets.setupBookmarkStatus
import dagger.hilt.android.AndroidEntryPoint
/**
* Displays a single event passed either as a complete Parcelable object in extras or as an id in data.
*
* @author Christophe Beyls
*/
@AndroidEntryPoint
class EventDetailsActivity : AppCompatActivity(R.layout.single_event), CreateNfcAppDataCallback {
private val bookmarkStatusViewModel: BookmarkStatusViewModel by viewModels()

View file

@ -8,7 +8,9 @@ import be.digitalia.fosdem.fragments.ExternalBookmarksListFragment
import be.digitalia.fosdem.utils.extractNfcAppData
import be.digitalia.fosdem.utils.hasNfcAppData
import be.digitalia.fosdem.utils.toBookmarks
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ExternalBookmarksActivity : SimpleToolbarActivity() {
override fun onCreate(savedInstanceState: Bundle?) {

View file

@ -33,7 +33,7 @@ import be.digitalia.fosdem.BuildConfig
import be.digitalia.fosdem.R
import be.digitalia.fosdem.api.FosdemApi
import be.digitalia.fosdem.api.FosdemUrls
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.db.ScheduleDao
import be.digitalia.fosdem.fragments.BookmarksListFragment
import be.digitalia.fosdem.fragments.LiveFragment
import be.digitalia.fosdem.fragments.MapFragment
@ -49,13 +49,16 @@ import be.digitalia.fosdem.utils.setNfcAppDataPushMessageCallbackIfAvailable
import com.google.android.material.navigation.NavigationView
import com.google.android.material.progressindicator.BaseProgressIndicator
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CancellationException
import javax.inject.Inject
/**
* Main entry point of the application. Allows to switch between section fragments and update the database.
*
* @author Christophe Beyls
*/
@AndroidEntryPoint
class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback {
private enum class Section(val fragmentClass: Class<out Fragment>,
@ -79,6 +82,11 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
val drawerLayout: DrawerLayout,
val navigationView: NavigationView)
@Inject
lateinit var api: FosdemApi
@Inject
lateinit var scheduleDao: ScheduleDao
private lateinit var holder: ViewHolder
private lateinit var drawerToggle: ActionBarDrawerToggle
private var searchMenuItem: MenuItem? = null
@ -93,7 +101,7 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
val progressIndicator: BaseProgressIndicator<*> = findViewById(R.id.progress)
// Monitor the schedule download
FosdemApi.downloadScheduleState.observe(this) { state ->
api.downloadScheduleState.observe(this) { state ->
when (state) {
is LoadingState.Loading -> {
with(progressIndicator) {
@ -119,7 +127,7 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
val snackbar = when (result) {
is DownloadScheduleResult.Error -> {
Snackbar.make(contentView, R.string.schedule_loading_error, ERROR_MESSAGE_DISPLAY_DURATION)
.setAction(R.string.schedule_loading_retry_action) { FosdemApi.downloadSchedule(this) }
.setAction(R.string.schedule_loading_retry_action) { api.downloadSchedule() }
}
is DownloadScheduleResult.UpToDate -> {
Snackbar.make(contentView, R.string.events_download_up_to_date, Snackbar.LENGTH_LONG)
@ -174,7 +182,7 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
// Latest update date, below the list
val latestUpdateTextView: TextView = navigationView.findViewById(R.id.latest_update)
AppDatabase.getInstance(this).scheduleDao.latestUpdateTime
scheduleDao.latestUpdateTime
.observe(this) { time ->
val timeString = if (time == -1L) getString(R.string.never)
else DateFormat.format(LATEST_UPDATE_DATE_FORMAT, time)
@ -238,7 +246,7 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
// Scheduled database update
val now = System.currentTimeMillis()
val latestUpdateTime = AppDatabase.getInstance(this).scheduleDao.latestUpdateTime.value
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)
@ -247,7 +255,7 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
putLong(PREF_LATEST_AUTO_UPDATE_ATTEMPT_TIME, now)
}
// Try to update immediately. If it fails, the user gets a message and a retry button.
FosdemApi.downloadSchedule(this)
api.downloadSchedule()
}
}
}
@ -289,7 +297,7 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
item.icon = icon
icon.start()
}
FosdemApi.downloadSchedule(this)
api.downloadSchedule()
true
}
else -> false

View file

@ -15,7 +15,9 @@ import be.digitalia.fosdem.model.Person
import be.digitalia.fosdem.utils.DateUtils
import be.digitalia.fosdem.utils.configureToolbarColors
import be.digitalia.fosdem.viewmodels.PersonInfoViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class PersonInfoActivity : AppCompatActivity(R.layout.person_info) {
private val viewModel: PersonInfoViewModel by viewModels()

View file

@ -19,6 +19,8 @@ import be.digitalia.fosdem.utils.configureToolbarColors
import be.digitalia.fosdem.utils.invertImageColors
import be.digitalia.fosdem.utils.isLightTheme
import be.digitalia.fosdem.utils.toSlug
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
/**
* A special Activity which is displayed like a dialog and shows a room image.
@ -26,8 +28,12 @@ import be.digitalia.fosdem.utils.toSlug
*
* @author Christophe Beyls
*/
@AndroidEntryPoint
class RoomImageDialogActivity : AppCompatActivity(R.layout.dialog_room_image) {
@Inject
lateinit var api: FosdemApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = intent
@ -40,14 +46,14 @@ class RoomImageDialogActivity : AppCompatActivity(R.layout.dialog_room_image) {
}
setImageResource(intent.getIntExtra(EXTRA_ROOM_IMAGE_RESOURCE_ID, 0))
}
configureToolbar(this, findViewById(R.id.toolbar), roomName)
configureToolbar(api, this, findViewById(R.id.toolbar), roomName)
}
companion object {
const val EXTRA_ROOM_NAME = "roomName"
const val EXTRA_ROOM_IMAGE_RESOURCE_ID = "imageResId"
fun configureToolbar(owner: LifecycleOwner, toolbar: Toolbar, roomName: String) {
fun configureToolbar(api: FosdemApi, owner: LifecycleOwner, toolbar: Toolbar, roomName: String) {
toolbar.title = roomName
if (roomName.isNotEmpty()) {
val context = toolbar.context
@ -72,7 +78,7 @@ class RoomImageDialogActivity : AppCompatActivity(R.layout.dialog_room_image) {
}
// Display the room status as subtitle
FosdemApi.getRoomStatuses(toolbar.context).observe(owner) { roomStatuses ->
api.roomStatuses.observe(owner) { roomStatuses ->
val roomStatus = roomStatuses[roomName]
toolbar.subtitle = if (roomStatus != null) {
SpannableString(context.getString(roomStatus.nameResId)).apply {

View file

@ -14,7 +14,9 @@ import be.digitalia.fosdem.fragments.SearchResultListFragment
import be.digitalia.fosdem.viewmodels.SearchViewModel
import be.digitalia.fosdem.viewmodels.SearchViewModel.Result.QueryTooShort
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class SearchResultActivity : SimpleToolbarActivity() {
private val viewModel: SearchViewModel by viewModels()

View file

@ -29,12 +29,14 @@ import be.digitalia.fosdem.utils.toNfcAppData
import be.digitalia.fosdem.viewmodels.BookmarkStatusViewModel
import be.digitalia.fosdem.viewmodels.TrackScheduleViewModel
import be.digitalia.fosdem.widgets.setupBookmarkStatus
import dagger.hilt.android.AndroidEntryPoint
/**
* Track Schedule container, works in both single pane and dual pane modes.
*
* @author Christophe Beyls
*/
@AndroidEntryPoint
class TrackScheduleActivity : AppCompatActivity(R.layout.track_schedule), CreateNfcAppDataCallback {
private val viewModel: TrackScheduleViewModel by viewModels()

View file

@ -33,12 +33,14 @@ import be.digitalia.fosdem.viewmodels.BookmarkStatusViewModel
import be.digitalia.fosdem.viewmodels.TrackScheduleEventViewModel
import be.digitalia.fosdem.widgets.ContentLoadingViewMediator
import be.digitalia.fosdem.widgets.setupBookmarkStatus
import dagger.hilt.android.AndroidEntryPoint
/**
* Event view of the track schedule; allows to slide between events of the same track using a ViewPager.
*
* @author Christophe Beyls
*/
@AndroidEntryPoint
class TrackScheduleEventActivity : AppCompatActivity(R.layout.track_schedule_event), CreateNfcAppDataCallback {
private val bookmarkStatusViewModel: BookmarkStatusViewModel by viewModels()

View file

@ -15,29 +15,31 @@ import androidx.collection.SimpleArrayMap
import androidx.core.content.ContextCompat
import androidx.core.text.set
import androidx.core.view.isGone
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
import be.digitalia.fosdem.R
import be.digitalia.fosdem.activities.EventDetailsActivity
import be.digitalia.fosdem.api.FosdemApi
import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.model.RoomStatus
import be.digitalia.fosdem.utils.DateUtils
import be.digitalia.fosdem.widgets.MultiChoiceHelper
import java.text.DateFormat
class BookmarksAdapter(context: Context, owner: LifecycleOwner,
private val multiChoiceHelper: MultiChoiceHelper)
: ListAdapter<Event, BookmarksAdapter.ViewHolder>(DIFF_CALLBACK) {
class BookmarksAdapter(context: Context, private val multiChoiceHelper: MultiChoiceHelper) :
ListAdapter<Event, BookmarksAdapter.ViewHolder>(DIFF_CALLBACK) {
private val timeDateFormat = DateUtils.getTimeDateFormat(context)
@ColorInt
private val errorColor: Int
private val observers = SimpleArrayMap<AdapterDataObserver, BookmarksDataObserverWrapper>()
private var roomStatuses: Map<String, RoomStatus>? = null
var roomStatuses: Map<String, RoomStatus>? = null
set(value) {
field = value
notifyItemRangeChanged(0, itemCount, DETAILS_PAYLOAD)
}
init {
setHasStableIds(true)
@ -45,10 +47,6 @@ class BookmarksAdapter(context: Context, owner: LifecycleOwner,
errorColor = getColor(R.styleable.ErrorColors_colorError, 0)
recycle()
}
FosdemApi.getRoomStatuses(context).observe(owner) { statuses ->
roomStatuses = statuses
notifyItemRangeChanged(0, itemCount, DETAILS_PAYLOAD)
}
}
override fun getItemId(position: Int) = getItem(position).id
@ -59,7 +57,7 @@ class BookmarksAdapter(context: Context, owner: LifecycleOwner,
}
private fun getRoomStatus(event: Event): RoomStatus? {
return roomStatuses?.let { it[event.roomName] }
return roomStatuses?.get(event.roomName)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {

View file

@ -13,30 +13,26 @@ import androidx.core.content.ContextCompat
import androidx.core.text.set
import androidx.core.view.isGone
import androidx.core.widget.TextViewCompat
import androidx.lifecycle.LifecycleOwner
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.RecyclerView
import be.digitalia.fosdem.R
import be.digitalia.fosdem.activities.EventDetailsActivity
import be.digitalia.fosdem.api.FosdemApi
import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.model.RoomStatus
import be.digitalia.fosdem.model.StatusEvent
import be.digitalia.fosdem.utils.DateUtils
import java.text.DateFormat
class EventsAdapter constructor(context: Context, owner: LifecycleOwner, private val showDay: Boolean = true)
: PagedListAdapter<StatusEvent, EventsAdapter.ViewHolder>(DIFF_CALLBACK) {
class EventsAdapter constructor(context: Context, private val showDay: Boolean = true) :
PagedListAdapter<StatusEvent, EventsAdapter.ViewHolder>(DIFF_CALLBACK) {
private val timeDateFormat = DateUtils.getTimeDateFormat(context)
private var roomStatuses: Map<String, RoomStatus>? = null
init {
FosdemApi.getRoomStatuses(context).observe(owner) { statuses ->
roomStatuses = statuses
var roomStatuses: Map<String, RoomStatus>? = null
set(value) {
field = value
notifyItemRangeChanged(0, itemCount, DETAILS_PAYLOAD)
}
}
override fun getItemViewType(position: Int) = R.layout.item_event
@ -46,7 +42,7 @@ class EventsAdapter constructor(context: Context, owner: LifecycleOwner, private
}
private fun getRoomStatus(event: Event): RoomStatus? {
return roomStatuses?.let { it[event.roomName] }
return roomStatuses?.get(event.roomName)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {

View file

@ -1,6 +1,5 @@
package be.digitalia.fosdem.alarms
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
@ -9,16 +8,18 @@ import androidx.preference.PreferenceManager
import be.digitalia.fosdem.model.AlarmInfo
import be.digitalia.fosdem.services.AlarmIntentService
import be.digitalia.fosdem.utils.PreferenceKeys
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
/**
* This class monitors bookmarks and preferences changes to dispatch alarm update work to AlarmIntentService.
*
* @author Christophe Beyls
*/
@SuppressLint("StaticFieldLeak")
object FosdemAlarmManager {
@Singleton
class FosdemAlarmManager @Inject constructor(@ApplicationContext private val context: Context) {
private lateinit var context: Context
private val onSharedPreferenceChangeListener = OnSharedPreferenceChangeListener { sharedPreferences, key ->
when (key) {
PreferenceKeys.NOTIFICATIONS_ENABLED -> {
@ -35,8 +36,7 @@ object FosdemAlarmManager {
}
@MainThread
fun init(context: Context) {
this.context = context.applicationContext
fun init() {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.context)
isEnabled = sharedPreferences.getBoolean(PreferenceKeys.NOTIFICATIONS_ENABLED, false)
sharedPreferences.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener)

View file

@ -1,6 +1,5 @@
package be.digitalia.fosdem.api
import android.content.Context
import android.os.SystemClock
import android.text.format.DateUtils
import androidx.annotation.MainThread
@ -9,7 +8,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap
import be.digitalia.fosdem.alarms.FosdemAlarmManager
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.db.ScheduleDao
import be.digitalia.fosdem.livedata.LiveDataFactory.scheduler
import be.digitalia.fosdem.livedata.SingleEvent
import be.digitalia.fosdem.model.DownloadScheduleResult
@ -26,6 +25,8 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import okio.buffer
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.pow
/**
@ -33,18 +34,13 @@ import kotlin.math.pow
*
* @author Christophe Beyls
*/
object FosdemApi {
// 8:30 (local time)
private const val DAY_START_TIME = 8 * DateUtils.HOUR_IN_MILLIS + 30 * DateUtils.MINUTE_IN_MILLIS
// 19:00 (local time)
private const val DAY_END_TIME = 19 * DateUtils.HOUR_IN_MILLIS
private const val ROOM_STATUS_REFRESH_DELAY = 90L * DateUtils.SECOND_IN_MILLIS
private const val ROOM_STATUS_FIRST_RETRY_DELAY = 30L * DateUtils.SECOND_IN_MILLIS
private const val ROOM_STATUS_EXPIRATION_DELAY = 6L * DateUtils.MINUTE_IN_MILLIS
@Singleton
class FosdemApi @Inject constructor(
private val scheduleDao: ScheduleDao,
private val alarmManager: FosdemAlarmManager
) {
private var downloadJob: Job? = null
private val _downloadScheduleState = MutableLiveData<LoadingState<DownloadScheduleResult>>()
private var roomStatuses: LiveData<Map<String, RoomStatus>>? = null
/**
* Download & store the schedule to the database.
@ -52,12 +48,11 @@ object FosdemApi {
* The result will be sent back through downloadScheduleResult LiveData.
*/
@MainThread
fun downloadSchedule(context: Context): Job {
fun downloadSchedule(): Job {
// Returns the download job in progress, if any
return downloadJob ?: run {
val appContext = context.applicationContext
BackgroundWorkScope.launch {
downloadScheduleInternal(appContext)
downloadScheduleInternal()
downloadJob = null
}.also {
downloadJob = it
@ -66,10 +61,9 @@ object FosdemApi {
}
@MainThread
private suspend fun downloadScheduleInternal(context: Context) {
private suspend fun downloadScheduleInternal() {
_downloadScheduleState.value = LoadingState.Loading()
val res = try {
val scheduleDao = AppDatabase.getInstance(context).scheduleDao
val response = HttpUtils.get(FosdemUrls.schedule, scheduleDao.lastModifiedTag) { body, rawResponse ->
val length = body.contentLength()
val source = if (length > 0L) {
@ -89,7 +83,7 @@ object FosdemApi {
when (response) {
is HttpUtils.Response.NotModified -> DownloadScheduleResult.UpToDate // Nothing parsed, the result is up-to-date
is HttpUtils.Response.Success -> {
FosdemAlarmManager.onScheduleRefreshed()
alarmManager.onScheduleRefreshed()
DownloadScheduleResult.Success(response.body)
}
}
@ -102,28 +96,24 @@ object FosdemApi {
val downloadScheduleState: LiveData<LoadingState<DownloadScheduleResult>>
get() = _downloadScheduleState
@MainThread
fun getRoomStatuses(context: Context): LiveData<Map<String, RoomStatus>> {
return roomStatuses ?: run {
// The room statuses will only be loaded when the event is live.
// Use the days from the database to determine it.
val scheduler = AppDatabase.getInstance(context).scheduleDao.days.switchMap { days ->
val startEndTimestamps = LongArray(days.size * 2)
var index = 0
for (day in days) {
val dayStart = day.date.time
startEndTimestamps[index++] = dayStart + DAY_START_TIME
startEndTimestamps[index++] = dayStart + DAY_END_TIME
}
scheduler(*startEndTimestamps)
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 startEndTimestamps = LongArray(days.size * 2)
var index = 0
for (day in days) {
val dayStart = day.date.time
startEndTimestamps[index++] = dayStart + DAY_START_TIME
startEndTimestamps[index++] = dayStart + DAY_END_TIME
}
val liveRoomStatuses = buildLiveRoomStatusesLiveData()
val offlineRoomStatuses = MutableLiveData(emptyMap<String, RoomStatus>())
scheduler.switchMap { isLive -> if (isLive) liveRoomStatuses else offlineRoomStatuses }
.also { roomStatuses = it }
// Implementors: replace the above code with the next line to disable room status support
// MutableLiveData().also { roomStatuses = it }
scheduler(*startEndTimestamps)
}
val liveRoomStatuses = buildLiveRoomStatusesLiveData()
val offlineRoomStatuses = MutableLiveData(emptyMap<String, RoomStatus>())
scheduler.switchMap { isLive -> if (isLive) liveRoomStatuses else offlineRoomStatuses }
// Implementors: replace the above code block with the next line to disable room status support
// MutableLiveData()
}
/**
@ -178,4 +168,15 @@ object FosdemApi {
}
}
}
companion object {
// 8:30 (local time)
private const val DAY_START_TIME = 8 * DateUtils.HOUR_IN_MILLIS + 30 * DateUtils.MINUTE_IN_MILLIS
// 19:00 (local time)
private const val DAY_END_TIME = 19 * DateUtils.HOUR_IN_MILLIS
private const val ROOM_STATUS_REFRESH_DELAY = 90L * DateUtils.SECOND_IN_MILLIS
private const val ROOM_STATUS_FIRST_RETRY_DELAY = 30L * DateUtils.SECOND_IN_MILLIS
private const val ROOM_STATUS_EXPIRATION_DELAY = 6L * DateUtils.MINUTE_IN_MILLIS
}
}

View file

@ -1,13 +1,11 @@
package be.digitalia.fosdem.db
import android.content.Context
import android.content.SharedPreferences
import androidx.room.Database
import androidx.room.Room
import androidx.room.DatabaseConfiguration
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import be.digitalia.fosdem.alarms.FosdemAlarmManager
import be.digitalia.fosdem.db.converters.GlobalTypeConverters
import be.digitalia.fosdem.db.entities.Bookmark
import be.digitalia.fosdem.db.entities.EventEntity
@ -17,63 +15,40 @@ 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 be.digitalia.fosdem.utils.SingletonHolder
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,
Link::class, Track::class, Day::class, Bookmark::class], version = 2, exportSchema = false)
@Database(
entities = [EventEntity::class, EventTitles::class, Person::class, EventToPerson::class,
Link::class, Track::class, Day::class, Bookmark::class], version = 2, exportSchema = false
)
@TypeConverters(GlobalTypeConverters::class)
abstract class AppDatabase : RoomDatabase() {
lateinit var sharedPreferences: SharedPreferences
private set
abstract val scheduleDao: ScheduleDao
abstract val bookmarksDao: BookmarksDao
companion object : SingletonHolder<AppDatabase, Context>({ context ->
val DB_FILE = "fosdem.sqlite"
val DB_PREFS_FILE = "database"
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}")
}
}
lateinit var sharedPreferences: SharedPreferences
private set
lateinit var alarmManager: FosdemAlarmManager
private set
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, DB_FILE)
.addMigrations(MIGRATION_1_2)
.setJournalMode(JournalMode.TRUNCATE)
.build().apply {
sharedPreferences = context.applicationContext.getSharedPreferences(DB_PREFS_FILE, Context.MODE_PRIVATE)
}
})
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: FosdemAlarmManager
}
}

View file

@ -7,7 +7,6 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.withTransaction
import be.digitalia.fosdem.alarms.FosdemAlarmManager
import be.digitalia.fosdem.db.entities.Bookmark
import be.digitalia.fosdem.model.AlarmInfo
import be.digitalia.fosdem.model.Event
@ -16,7 +15,6 @@ import kotlinx.coroutines.launch
@Dao
abstract class BookmarksDao(private val appDatabase: AppDatabase) {
/**
* Returns the bookmarks.
*
@ -65,7 +63,7 @@ abstract class BookmarksDao(private val appDatabase: AppDatabase) {
BackgroundWorkScope.launch {
val ids = addBookmarksInternal(listOf(Bookmark(event.id)))
if (ids[0] != -1L) {
FosdemAlarmManager.onBookmarksAdded(listOf(AlarmInfo(eventId = event.id, startTime = event.startTime)))
appDatabase.alarmManager.onBookmarksAdded(listOf(AlarmInfo(eventId = event.id, startTime = event.startTime)))
}
}
}
@ -81,7 +79,7 @@ abstract class BookmarksDao(private val appDatabase: AppDatabase) {
// Filter out items that were already in bookmarks
val addedAlarmInfos = alarmInfos.filterIndexed { index, _ -> ids[index] != -1L }
if (addedAlarmInfos.isNotEmpty()) {
FosdemAlarmManager.onBookmarksAdded(addedAlarmInfos)
appDatabase.alarmManager.onBookmarksAdded(addedAlarmInfos)
}
}
}
@ -103,7 +101,7 @@ abstract class BookmarksDao(private val appDatabase: AppDatabase) {
fun removeBookmarksAsync(eventIds: LongArray) {
BackgroundWorkScope.launch {
if (removeBookmarksInternal(eventIds) > 0) {
FosdemAlarmManager.onBookmarksRemoved(eventIds)
appDatabase.alarmManager.onBookmarksRemoved(eventIds)
}
}
}

View file

@ -268,8 +268,7 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
LEFT JOIN persons p ON ep.person_id = p.`rowid`
WHERE e.id = :id
GROUP BY e.id""")
@WorkerThread
abstract fun getEvent(id: Long): Event?
abstract suspend fun getEvent(id: Long): Event?
/**
* Returns all found events whose id is part of the given list.
@ -321,7 +320,7 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
WHERE e.day_index = :day AND e.track_id = :track
GROUP BY e.id
ORDER BY e.start_time ASC""")
abstract fun getEventsSnapshot(day: Day, track: Track): List<Event>
abstract suspend fun getEventsSnapshot(day: Day, track: Track): List<Event>
/**
* Returns events starting in the specified interval, ordered by ascending start time.

View file

@ -23,21 +23,27 @@ import androidx.recyclerview.widget.LinearLayoutManager
import be.digitalia.fosdem.R
import be.digitalia.fosdem.activities.ExternalBookmarksActivity
import be.digitalia.fosdem.adapters.BookmarksAdapter
import be.digitalia.fosdem.api.FosdemApi
import be.digitalia.fosdem.providers.BookmarksExportProvider
import be.digitalia.fosdem.utils.CreateNfcAppDataCallback
import be.digitalia.fosdem.utils.toBookmarksNfcAppData
import be.digitalia.fosdem.viewmodels.BookmarksViewModel
import be.digitalia.fosdem.widgets.MultiChoiceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import java.util.concurrent.CancellationException
import javax.inject.Inject
/**
* Bookmarks list, optionally filterable.
*
* @author Christophe Beyls
*/
@AndroidEntryPoint
class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataCallback {
@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 {
@ -94,7 +100,7 @@ class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataC
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = BookmarksAdapter(view.context, viewLifecycleOwner, multiChoiceHelper)
val adapter = BookmarksAdapter(view.context, multiChoiceHelper)
val holder = RecyclerViewViewHolder(view).apply {
recyclerView.apply {
layoutManager = LinearLayoutManager(recyclerView.context)
@ -105,6 +111,9 @@ class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataC
isProgressBarVisible = true
}
api.roomStatuses.observe(viewLifecycleOwner) { statuses ->
adapter.roomStatuses = statuses
}
viewModel.bookmarks.observe(viewLifecycleOwner) { bookmarks ->
adapter.submitList(bookmarks)
multiChoiceHelper.setAdapter(adapter, viewLifecycleOwner)

View file

@ -44,7 +44,10 @@ import be.digitalia.fosdem.utils.roomNameToResourceName
import be.digitalia.fosdem.utils.stripHtml
import be.digitalia.fosdem.viewmodels.EventDetailsViewModel
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class EventDetailsFragment : Fragment(R.layout.fragment_event_details) {
private class ViewHolder(view: View) {
@ -54,6 +57,8 @@ class EventDetailsFragment : Fragment(R.layout.fragment_event_details) {
val linksContainer: ViewGroup = view.findViewById(R.id.links_container)
}
@Inject
lateinit var api: FosdemApi
private val viewModel: EventDetailsViewModel by viewModels()
val event by lazy<Event>(LazyThreadSafetyMode.NONE) {
@ -163,7 +168,7 @@ class EventDetailsFragment : Fragment(R.layout.fragment_event_details) {
val roomName = event.roomName
if (!roomName.isNullOrEmpty()) {
holder.roomStatusTextView.run {
FosdemApi.getRoomStatuses(context).observe(viewLifecycleOwner) { roomStatuses ->
api.roomStatuses.observe(viewLifecycleOwner) { roomStatuses ->
val roomStatus = roomStatuses[roomName]
if (roomStatus == null) {
text = null

View file

@ -15,11 +15,17 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import be.digitalia.fosdem.R
import be.digitalia.fosdem.adapters.EventsAdapter
import be.digitalia.fosdem.api.FosdemApi
import be.digitalia.fosdem.viewmodels.ExternalBookmarksViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class ExternalBookmarksListFragment : Fragment(R.layout.recyclerview) {
@Inject
lateinit var api: FosdemApi
private val viewModel: ExternalBookmarksViewModel by viewModels()
private var addAllMenuItem: MenuItem? = null
@ -32,7 +38,7 @@ class ExternalBookmarksListFragment : Fragment(R.layout.recyclerview) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = EventsAdapter(view.context, viewLifecycleOwner)
val adapter = EventsAdapter(view.context)
val holder = RecyclerViewViewHolder(view).apply {
recyclerView.apply {
layoutManager = LinearLayoutManager(context)
@ -45,6 +51,9 @@ class ExternalBookmarksListFragment : Fragment(R.layout.recyclerview) {
val bookmarkIds = requireArguments().getLongArray(ARG_BOOKMARK_IDS)!!
api.roomStatuses.observe(viewLifecycleOwner) { statuses ->
adapter.roomStatuses = statuses
}
with(viewModel) {
setBookmarkIds(bookmarkIds)
bookmarks.observe(viewLifecycleOwner) { bookmarks ->

View file

@ -12,7 +12,9 @@ import be.digitalia.fosdem.utils.recyclerView
import be.digitalia.fosdem.utils.viewLifecycleLazy
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class LiveFragment : Fragment(R.layout.fragment_live), RecycledViewPoolProvider {
private class ViewHolder(view: View) {

View file

@ -11,19 +11,25 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import be.digitalia.fosdem.R
import be.digitalia.fosdem.adapters.EventsAdapter
import be.digitalia.fosdem.api.FosdemApi
import be.digitalia.fosdem.model.StatusEvent
import be.digitalia.fosdem.viewmodels.LiveViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
sealed class LiveListFragment(@StringRes private val emptyTextResId: Int,
private val dataSourceProvider: (LiveViewModel) -> LiveData<PagedList<StatusEvent>>)
: Fragment(R.layout.recyclerview) {
@Inject
lateinit var api: FosdemApi
private val viewModel: LiveViewModel by viewModels({ requireParentFragment() })
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = EventsAdapter(view.context, viewLifecycleOwner, false)
val adapter = EventsAdapter(view.context, false)
val holder = RecyclerViewViewHolder(view).apply {
recyclerView.apply {
val parent = parentFragment
@ -39,6 +45,9 @@ sealed class LiveListFragment(@StringRes private val emptyTextResId: Int,
isProgressBarVisible = true
}
api.roomStatuses.observe(viewLifecycleOwner) { statuses ->
adapter.roomStatuses = statuses
}
dataSourceProvider(viewModel).observe(viewLifecycleOwner) { events ->
adapter.submitList(events) {
// Ensure we stay at scroll position 0 so we can see the insertion animation

View file

@ -5,22 +5,29 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import be.digitalia.fosdem.R
import be.digitalia.fosdem.adapters.EventsAdapter
import be.digitalia.fosdem.api.FosdemApi
import be.digitalia.fosdem.viewmodels.PersonInfoViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class PersonInfoListFragment : Fragment(R.layout.recyclerview) {
@Inject
lateinit var api: FosdemApi
// Fetch data from parent Activity's ViewModel
private val viewModel: PersonInfoViewModel by viewModels({ requireActivity() })
private val viewModel: PersonInfoViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = EventsAdapter(view.context, viewLifecycleOwner)
val adapter = EventsAdapter(view.context)
val holder = RecyclerViewViewHolder(view).apply {
recyclerView.apply {
val contentMargin = resources.getDimensionPixelSize(R.dimen.content_margin)
@ -37,6 +44,9 @@ class PersonInfoListFragment : Fragment(R.layout.recyclerview) {
isProgressBarVisible = true
}
api.roomStatuses.observe(viewLifecycleOwner) { statuses ->
adapter.roomStatuses = statuses
}
viewModel.events.observe(viewLifecycleOwner) { events ->
adapter.submitList(events)
holder.isProgressBarVisible = false

View file

@ -17,7 +17,9 @@ import be.digitalia.fosdem.activities.PersonInfoActivity
import be.digitalia.fosdem.adapters.createSimpleItemCallback
import be.digitalia.fosdem.model.Person
import be.digitalia.fosdem.viewmodels.PersonsViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class PersonsListFragment : Fragment(R.layout.recyclerview_fastscroll) {
private val viewModel: PersonsViewModel by viewModels()

View file

@ -10,12 +10,19 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import be.digitalia.fosdem.R
import be.digitalia.fosdem.activities.RoomImageDialogActivity
import be.digitalia.fosdem.api.FosdemApi
import be.digitalia.fosdem.utils.invertImageColors
import be.digitalia.fosdem.utils.isLightTheme
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class RoomImageDialogFragment : DialogFragment() {
@Inject
lateinit var api: FosdemApi
@SuppressLint("InflateParams")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val args = requireArguments()
@ -29,7 +36,12 @@ class RoomImageDialogFragment : DialogFragment() {
}
setImageResource(args.getInt(ARG_ROOM_IMAGE_RESOURCE_ID))
}
RoomImageDialogActivity.configureToolbar(this, contentView.findViewById(R.id.toolbar), args.getString(ARG_ROOM_NAME)!!)
RoomImageDialogActivity.configureToolbar(
api,
this,
contentView.findViewById(R.id.toolbar),
args.getString(ARG_ROOM_NAME)!!
)
return dialogBuilder
.setView(contentView)

View file

@ -8,16 +8,22 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import be.digitalia.fosdem.R
import be.digitalia.fosdem.adapters.EventsAdapter
import be.digitalia.fosdem.api.FosdemApi
import be.digitalia.fosdem.viewmodels.SearchViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class SearchResultListFragment : Fragment(R.layout.recyclerview) {
@Inject
lateinit var api: FosdemApi
private val viewModel: SearchViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = EventsAdapter(view.context, viewLifecycleOwner)
val adapter = EventsAdapter(view.context)
val holder = RecyclerViewViewHolder(view).apply {
recyclerView.apply {
layoutManager = LinearLayoutManager(context)
@ -28,6 +34,9 @@ class SearchResultListFragment : Fragment(R.layout.recyclerview) {
isProgressBarVisible = true
}
api.roomStatuses.observe(viewLifecycleOwner) { statuses ->
adapter.roomStatuses = statuses
}
viewModel.results.observe(viewLifecycleOwner) { result ->
adapter.submitList((result as? SearchViewModel.Result.Success)?.list)
holder.isProgressBarVisible = false

View file

@ -17,7 +17,9 @@ import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.model.Track
import be.digitalia.fosdem.viewmodels.TrackScheduleListViewModel
import be.digitalia.fosdem.viewmodels.TrackScheduleViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class TrackScheduleListFragment : Fragment(R.layout.recyclerview), TrackScheduleAdapter.EventClickListener {
private val viewModel: TrackScheduleListViewModel by viewModels()

View file

@ -12,7 +12,7 @@ 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.AppDatabase
import be.digitalia.fosdem.db.ScheduleDao
import be.digitalia.fosdem.model.Day
import be.digitalia.fosdem.utils.enforceSingleScrollDirection
import be.digitalia.fosdem.utils.instantiate
@ -20,7 +20,10 @@ import be.digitalia.fosdem.utils.recyclerView
import be.digitalia.fosdem.utils.viewLifecycleLazy
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvider {
private class ViewHolder(view: View) {
@ -30,6 +33,9 @@ class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvi
val tabs: TabLayout = view.findViewById(R.id.tabs)
}
@Inject
lateinit var scheduleDao: ScheduleDao
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -46,7 +52,7 @@ class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvi
requireActivity().getPreferences(Context.MODE_PRIVATE).getInt(PREF_CURRENT_PAGE, -1)
} else -1
AppDatabase.getInstance(requireContext()).scheduleDao.days.observe(viewLifecycleOwner) { days ->
scheduleDao.days.observe(viewLifecycleOwner) { days ->
holder.run {
daysAdapter.days = days

View file

@ -19,7 +19,9 @@ 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 dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class TracksListFragment : Fragment(R.layout.recyclerview) {
private val viewModel: TracksViewModel by viewModels()

View file

@ -0,0 +1,87 @@
package be.digitalia.fosdem.inject
import android.content.Context
import android.content.SharedPreferences
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
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 javax.inject.Named
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
private const val DB_FILE = "fosdem.sqlite"
private const val DB_PREFS_FILE = "database"
@Provides
@Named("Database")
fun providesSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
return context.applicationContext.getSharedPreferences(DB_PREFS_FILE, Context.MODE_PRIVATE)
}
@Provides
@Singleton
fun providesAppDatabase(@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}")
}
}
return Room.databaseBuilder(context, AppDatabase::class.java, DB_FILE)
.addMigrations(MIGRATION_1_2)
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
.build()
}
@Provides
fun providesScheduleDao(appDatabase: AppDatabase): ScheduleDao = appDatabase.scheduleDao
@Provides
fun providesBookmarksDao(appDatabase: AppDatabase): BookmarksDao = appDatabase.bookmarksDao
}

View file

@ -10,15 +10,21 @@ import android.net.Uri
import android.os.ParcelFileDescriptor
import android.provider.OpenableColumns
import androidx.core.app.ShareCompat
import androidx.core.content.ContentProviderCompat
import be.digitalia.fosdem.BuildConfig
import be.digitalia.fosdem.R
import be.digitalia.fosdem.api.FosdemUrls
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.db.BookmarksDao
import be.digitalia.fosdem.db.ScheduleDao
import be.digitalia.fosdem.ical.ICalendarWriter
import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.utils.DateUtils
import be.digitalia.fosdem.utils.stripHtml
import be.digitalia.fosdem.utils.toSlug
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import okio.buffer
import okio.sink
import java.io.FileNotFoundException
@ -34,6 +40,19 @@ import java.util.TimeZone
*/
class BookmarksExportProvider : ContentProvider() {
private val scheduleDao: ScheduleDao by lazy {
EntryPointAccessors.fromApplication(
ContentProviderCompat.requireContext(this),
BookmarksExportProviderEntryPoint::class.java
).scheduleDao
}
private val bookmarksDao: BookmarksDao by lazy {
EntryPointAccessors.fromApplication(
ContentProviderCompat.requireContext(this),
BookmarksExportProviderEntryPoint::class.java
).bookmarksDao
}
override fun onCreate() = true
override fun insert(uri: Uri, values: ContentValues?) = throw UnsupportedOperationException()
@ -45,7 +64,7 @@ class BookmarksExportProvider : ContentProvider() {
override fun getType(uri: Uri) = TYPE
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
val ctx = context!!
val ctx = ContentProviderCompat.requireContext(this)
val proj = projection ?: COLUMNS
val cols = arrayOfNulls<String>(proj.size)
val values = arrayOfNulls<Any>(proj.size)
@ -54,7 +73,7 @@ class BookmarksExportProvider : ContentProvider() {
when (col) {
OpenableColumns.DISPLAY_NAME -> {
cols[columnCount] = OpenableColumns.DISPLAY_NAME
values[columnCount++] = ctx.getString(R.string.export_bookmarks_file_name, AppDatabase.getInstance(ctx).scheduleDao.getYear())
values[columnCount++] = ctx.getString(R.string.export_bookmarks_file_name, scheduleDao.getYear())
}
OpenableColumns.SIZE -> {
cols[columnCount] = OpenableColumns.SIZE
@ -72,17 +91,14 @@ class BookmarksExportProvider : ContentProvider() {
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
return try {
val pipe = ParcelFileDescriptor.createPipe()
DownloadThread(
ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]),
AppDatabase.getInstance(context!!)
).start()
DownloadThread(ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]), bookmarksDao).start()
pipe[0]
} catch (e: IOException) {
throw FileNotFoundException("Could not open pipe")
}
}
private class DownloadThread(private val outputStream: OutputStream, private val appDatabase: AppDatabase) : Thread() {
private class DownloadThread(private val outputStream: OutputStream, private val bookmarksDao: BookmarksDao) : Thread() {
private val calendar = Calendar.getInstance(DateUtils.belgiumTimeZone, Locale.US)
// Format all times in GMT
private val dateFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US).apply {
@ -93,7 +109,7 @@ class BookmarksExportProvider : ContentProvider() {
override fun run() {
try {
ICalendarWriter(outputStream.sink().buffer()).use { writer ->
val bookmarks = appDatabase.bookmarksDao.getBookmarks()
val bookmarks = bookmarksDao.getBookmarks()
writer.write("BEGIN", "VCALENDAR")
writer.write("VERSION", "2.0")
writer.write("PRODID", "-//${BuildConfig.APPLICATION_ID}//NONSGML ${BuildConfig.VERSION_NAME}//EN")
@ -143,6 +159,13 @@ class BookmarksExportProvider : ContentProvider() {
}
}
@EntryPoint
@InstallIn(SingletonComponent::class)
interface BookmarksExportProviderEntryPoint {
val scheduleDao: ScheduleDao
val bookmarksDao: BookmarksDao
}
companion object {
const val TYPE = "text/calendar"
private val URI = Uri.Builder()

View file

@ -5,7 +5,12 @@ import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import be.digitalia.fosdem.db.AppDatabase
import androidx.core.content.ContentProviderCompat
import be.digitalia.fosdem.db.ScheduleDao
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
/**
* Simple content provider responsible for search suggestions.
@ -14,25 +19,22 @@ import be.digitalia.fosdem.db.AppDatabase
*/
class SearchSuggestionProvider : ContentProvider() {
override fun onCreate(): Boolean {
return true
private val scheduleDao: ScheduleDao by lazy {
EntryPointAccessors.fromApplication(
ContentProviderCompat.requireContext(this),
SearchSuggestionProviderEntryPoint::class.java
).scheduleDao
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
throw UnsupportedOperationException()
}
override fun onCreate() = true
override fun insert(uri: Uri, values: ContentValues?): Uri? {
throw UnsupportedOperationException()
}
override fun insert(uri: Uri, values: ContentValues?) = throw UnsupportedOperationException()
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
throw UnsupportedOperationException()
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?) = throw UnsupportedOperationException()
override fun getType(uri: Uri): String? {
return SearchManager.SUGGEST_MIME_TYPE
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) = throw UnsupportedOperationException()
override fun getType(uri: Uri) = SearchManager.SUGGEST_MIME_TYPE
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
var query = uri.lastPathSegment ?: return null
@ -45,7 +47,13 @@ class SearchSuggestionProvider : ContentProvider() {
val limitParam = uri.getQueryParameter("limit")
val limit = if (limitParam.isNullOrEmpty()) DEFAULT_MAX_RESULTS else limitParam.toInt()
return AppDatabase.getInstance(context!!).scheduleDao.getSearchSuggestionResults(query, limit)
return scheduleDao.getSearchSuggestionResults(query, limit)
}
@EntryPoint
@InstallIn(SingletonComponent::class)
interface SearchSuggestionProviderEntryPoint {
val scheduleDao: ScheduleDao
}
companion object {

View file

@ -6,23 +6,29 @@ import android.content.Intent
import be.digitalia.fosdem.BuildConfig
import be.digitalia.fosdem.alarms.FosdemAlarmManager
import be.digitalia.fosdem.services.AlarmIntentService
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
/**
* Entry point for system-generated events: boot complete and alarms.
*
* @author Christophe Beyls
*/
@AndroidEntryPoint
class AlarmReceiver : BroadcastReceiver() {
@Inject
lateinit var alarmManager: FosdemAlarmManager
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
ACTION_NOTIFY_EVENT -> {
val serviceIntent = Intent(ACTION_NOTIFY_EVENT)
.setData(intent.data)
.setData(intent.data)
AlarmIntentService.enqueueWork(context, serviceIntent)
}
Intent.ACTION_BOOT_COMPLETED -> {
val serviceAction = if (FosdemAlarmManager.isEnabled) AlarmIntentService.ACTION_UPDATE_ALARMS
val serviceAction = if (alarmManager.isEnabled) AlarmIntentService.ACTION_UPDATE_ALARMS
else AlarmIntentService.ACTION_DISABLE_ALARMS
val serviceIntent = Intent(serviceAction)
AlarmIntentService.enqueueWork(context, serviceIntent)

View file

@ -31,20 +31,30 @@ import be.digitalia.fosdem.R
import be.digitalia.fosdem.activities.EventDetailsActivity
import be.digitalia.fosdem.activities.MainActivity
import be.digitalia.fosdem.activities.RoomImageDialogActivity
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.db.BookmarksDao
import be.digitalia.fosdem.db.ScheduleDao
import be.digitalia.fosdem.model.AlarmInfo
import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.receivers.AlarmReceiver
import be.digitalia.fosdem.utils.PreferenceKeys
import be.digitalia.fosdem.utils.roomNameToResourceName
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
/**
* A service to schedule or unschedule alarms in the background, keeping the app responsive.
*
* @author Christophe Beyls
*/
@AndroidEntryPoint
class AlarmIntentService : JobIntentService() {
@Inject
lateinit var bookmarksDao: BookmarksDao
@Inject
lateinit var scheduleDao: ScheduleDao
private val alarmManager by lazy<AlarmManager> {
getSystemService()!!
}
@ -63,7 +73,7 @@ class AlarmIntentService : JobIntentService() {
val delay = delay
val now = System.currentTimeMillis()
var hasAlarms = false
for (info in AppDatabase.getInstance(this).bookmarksDao.getBookmarksAlarmInfo(0L)) {
for (info in bookmarksDao.getBookmarksAlarmInfo(0L)) {
val startTime = info.startTime
val notificationTime = if (startTime == null) -1L else startTime.time - delay
val pi = getAlarmPendingIntent(info.eventId)
@ -82,7 +92,7 @@ class AlarmIntentService : JobIntentService() {
}
ACTION_DISABLE_ALARMS -> {
// Cancel alarms of every bookmark in the future
for (info in AppDatabase.getInstance(this).bookmarksDao.getBookmarksAlarmInfo(System.currentTimeMillis())) {
for (info in bookmarksDao.getBookmarksAlarmInfo(System.currentTimeMillis())) {
alarmManager.cancel(getAlarmPendingIntent(info.eventId))
}
setAlarmReceiverEnabled(false)
@ -116,7 +126,7 @@ class AlarmIntentService : JobIntentService() {
}
AlarmReceiver.ACTION_NOTIFY_EVENT -> {
val eventId = intent.dataString!!.toLong()
val event = AppDatabase.getInstance(this).scheduleDao.getEvent(eventId)
val event = runBlocking { scheduleDao.getEvent(eventId) }
if (event != null) {
NotificationManagerCompat.from(this).notify(eventId.toInt(), buildNotification(event))
}

View file

@ -1,31 +0,0 @@
package be.digitalia.fosdem.utils
/**
* Kotlin Singleton with argument.
*
* @author Christophe Beyls
*/
open class SingletonHolder<out T : Any, in A>(creator: (A) -> T) {
private var creator: ((A) -> T)? = creator
@Volatile
private var instance: T? = null
fun getInstance(arg: A): T {
val i = instance
if (i != null) {
return i
}
return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = creator!!(arg)
instance = created
creator = null
created
}
}
}
}

View file

@ -1,19 +1,20 @@
package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import androidx.lifecycle.switchMap
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.db.BookmarksDao
import be.digitalia.fosdem.model.BookmarkStatus
import be.digitalia.fosdem.model.Event
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
class BookmarkStatusViewModel(application: Application) : AndroidViewModel(application) {
@HiltViewModel
class BookmarkStatusViewModel @Inject constructor(private val bookmarksDao: BookmarksDao) : ViewModel() {
private val appDatabase = AppDatabase.getInstance(application)
private val eventLiveData = MutableLiveData<Event?>()
private var firstResultReceived = false
@ -21,13 +22,13 @@ class BookmarkStatusViewModel(application: Application) : AndroidViewModel(appli
if (event == null) {
MutableLiveData(null)
} else {
appDatabase.bookmarksDao.getBookmarkStatus(event)
.distinctUntilChanged() // Prevent updating the UI when a bookmark is added back or removed back
.map { isBookmarked ->
val isUpdate = firstResultReceived
firstResultReceived = true
BookmarkStatus(isBookmarked, isUpdate)
}
bookmarksDao.getBookmarkStatus(event)
.distinctUntilChanged() // Prevent updating the UI when a bookmark is added back or removed back
.map { isBookmarked ->
val isUpdate = firstResultReceived
firstResultReceived = true
BookmarkStatus(isBookmarked, isUpdate)
}
}
}
@ -46,9 +47,9 @@ class BookmarkStatusViewModel(application: Application) : AndroidViewModel(appli
// Ignore the action if the status for the current event hasn't been received yet
if (event != null && currentStatus != null && firstResultReceived) {
if (currentStatus.isBookmarked) {
appDatabase.bookmarksDao.removeBookmarkAsync(event)
bookmarksDao.removeBookmarkAsync(event)
} else {
appDatabase.bookmarksDao.addBookmarkAsync(event)
bookmarksDao.addBookmarkAsync(event)
}
}
}

View file

@ -3,35 +3,42 @@ package be.digitalia.fosdem.viewmodels
import android.app.Application
import android.net.Uri
import android.text.format.DateUtils
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap
import be.digitalia.fosdem.BuildConfig
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.db.BookmarksDao
import be.digitalia.fosdem.db.ScheduleDao
import be.digitalia.fosdem.livedata.LiveDataFactory
import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.parsers.ExportedBookmarksParser
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okio.buffer
import okio.source
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class BookmarksViewModel(application: Application) : AndroidViewModel(application) {
@HiltViewModel
class BookmarksViewModel @Inject constructor(
private val bookmarksDao: BookmarksDao,
private val scheduleDao: ScheduleDao,
private val application: Application
) : ViewModel() {
private val appDatabase = AppDatabase.getInstance(application)
private val upcomingOnlyLiveData = MutableLiveData<Boolean>()
val bookmarks: LiveData<List<Event>> = upcomingOnlyLiveData.switchMap { upcomingOnly: Boolean ->
if (upcomingOnly) {
// Refresh upcoming bookmarks every 2 minutes
LiveDataFactory.interval(2L, TimeUnit.MINUTES)
.switchMap {
appDatabase.bookmarksDao.getBookmarks(System.currentTimeMillis() - TIME_OFFSET)
}
.switchMap {
bookmarksDao.getBookmarks(System.currentTimeMillis() - TIME_OFFSET)
}
} else {
appDatabase.bookmarksDao.getBookmarks(-1L)
bookmarksDao.getBookmarks(-1L)
}
}
@ -44,15 +51,14 @@ class BookmarksViewModel(application: Application) : AndroidViewModel(applicatio
}
fun removeBookmarks(eventIds: LongArray) {
appDatabase.bookmarksDao.removeBookmarksAsync(eventIds)
bookmarksDao.removeBookmarksAsync(eventIds)
}
suspend fun readBookmarkIds(uri: Uri): LongArray {
return withContext(Dispatchers.IO) {
val parser = ExportedBookmarksParser(BuildConfig.APPLICATION_ID, appDatabase.scheduleDao.getYear())
checkNotNull(getApplication<Application>().contentResolver.openInputStream(uri)).source().buffer().use {
parser.parse(it)
}
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun readBookmarkIds(uri: Uri): LongArray = withContext(Dispatchers.IO) {
val parser = ExportedBookmarksParser(BuildConfig.APPLICATION_ID, scheduleDao.getYear())
checkNotNull(application.contentResolver.openInputStream(uri)).source().buffer().use {
parser.parse(it)
}
}

View file

@ -1,21 +1,22 @@
package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.db.ScheduleDao
import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.model.EventDetails
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
class EventDetailsViewModel(application: Application) : AndroidViewModel(application) {
@HiltViewModel
class EventDetailsViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
private val appDatabase = AppDatabase.getInstance(application)
private val eventLiveData = MutableLiveData<Event>()
val eventDetails: LiveData<EventDetails> = eventLiveData.switchMap { event: Event ->
appDatabase.scheduleDao.getEventDetails(event)
scheduleDao.getEventDetails(event)
}
fun setEvent(event: Event) {

View file

@ -1,27 +1,28 @@
package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.db.ScheduleDao
import be.digitalia.fosdem.model.Event
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
class EventViewModel(application: Application) : AndroidViewModel(application) {
@HiltViewModel
class EventViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
private val appDatabase = AppDatabase.getInstance(application)
private val eventIdLiveData = MutableLiveData<Long>()
val event: LiveData<Event?> = eventIdLiveData.switchMap { id: Long ->
MutableLiveData<Event?>().also {
appDatabase.queryExecutor.execute {
it.postValue(appDatabase.scheduleDao.getEvent(id))
}
val event: LiveData<Event?> = eventIdLiveData.switchMap { id ->
liveData {
emit(scheduleDao.getEvent(id))
}
}
val isEventIdSet = eventIdLiveData.value != null
val isEventIdSet
get() = eventIdLiveData.value != null
fun setEventId(eventId: Long) {
if (eventId != eventIdLiveData.value) {

View file

@ -1,22 +1,27 @@
package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap
import androidx.paging.PagedList
import androidx.paging.toLiveData
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.db.BookmarksDao
import be.digitalia.fosdem.db.ScheduleDao
import be.digitalia.fosdem.model.StatusEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
class ExternalBookmarksViewModel(application: Application) : AndroidViewModel(application) {
@HiltViewModel
class ExternalBookmarksViewModel @Inject constructor(
scheduleDao: ScheduleDao,
private val bookmarksDao: BookmarksDao
) : ViewModel() {
private val appDatabase = AppDatabase.getInstance(application)
private val bookmarkIdsLiveData = MutableLiveData<LongArray>()
val bookmarks: LiveData<PagedList<StatusEvent>> = bookmarkIdsLiveData.switchMap { bookmarkIds: LongArray ->
appDatabase.scheduleDao.getEvents(bookmarkIds).toLiveData(20)
val bookmarks: LiveData<PagedList<StatusEvent>> = bookmarkIdsLiveData.switchMap { bookmarkIds ->
scheduleDao.getEvents(bookmarkIds).toLiveData(20)
}
fun setBookmarkIds(bookmarkIds: LongArray) {
@ -28,6 +33,6 @@ class ExternalBookmarksViewModel(application: Application) : AndroidViewModel(ap
fun addAll() {
val bookmarkIds = bookmarkIdsLiveData.value ?: return
appDatabase.bookmarksDao.addBookmarksAsync(bookmarkIds)
bookmarksDao.addBookmarksAsync(bookmarkIds)
}
}

View file

@ -1,29 +1,30 @@
package be.digitalia.fosdem.viewmodels
import android.app.Application
import android.text.format.DateUtils
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap
import androidx.paging.PagedList
import androidx.paging.toLiveData
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.db.ScheduleDao
import be.digitalia.fosdem.livedata.LiveDataFactory
import be.digitalia.fosdem.model.StatusEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class LiveViewModel(application: Application) : AndroidViewModel(application) {
@HiltViewModel
class LiveViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
private val appDatabase = AppDatabase.getInstance(application)
private val heartbeat = LiveDataFactory.interval(1L, TimeUnit.MINUTES)
val nextEvents: LiveData<PagedList<StatusEvent>> = heartbeat.switchMap {
val now = System.currentTimeMillis()
appDatabase.scheduleDao.getEventsWithStartTime(now, now + NEXT_EVENTS_INTERVAL).toLiveData(20)
scheduleDao.getEventsWithStartTime(now, now + NEXT_EVENTS_INTERVAL).toLiveData(20)
}
val eventsInProgress: LiveData<PagedList<StatusEvent>> = heartbeat.switchMap {
appDatabase.scheduleDao.getEventsInProgress(System.currentTimeMillis()).toLiveData(20)
scheduleDao.getEventsInProgress(System.currentTimeMillis()).toLiveData(20)
}
companion object {

View file

@ -1,23 +1,24 @@
package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap
import androidx.paging.PagedList
import androidx.paging.toLiveData
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.db.ScheduleDao
import be.digitalia.fosdem.model.Person
import be.digitalia.fosdem.model.StatusEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
class PersonInfoViewModel(application: Application) : AndroidViewModel(application) {
@HiltViewModel
class PersonInfoViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
private val appDatabase = AppDatabase.getInstance(application)
private val personLiveData = MutableLiveData<Person>()
val events: LiveData<PagedList<StatusEvent>> = personLiveData.switchMap { person: Person ->
appDatabase.scheduleDao.getEvents(person).toLiveData(20)
scheduleDao.getEvents(person).toLiveData(20)
}
fun setPerson(person: Person) {

View file

@ -1,16 +1,16 @@
package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.paging.PagedList
import androidx.paging.toLiveData
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.db.ScheduleDao
import be.digitalia.fosdem.model.Person
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
class PersonsViewModel(application: Application) : AndroidViewModel(application) {
@HiltViewModel
class PersonsViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
private val appDatabase = AppDatabase.getInstance(application)
val persons: LiveData<PagedList<Person>> = appDatabase.scheduleDao.getPersons().toLiveData(100)
val persons: LiveData<PagedList<Person>> = scheduleDao.getPersons().toLiveData(100)
}

View file

@ -1,20 +1,21 @@
package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import androidx.lifecycle.switchMap
import androidx.paging.PagedList
import androidx.paging.toLiveData
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.db.ScheduleDao
import be.digitalia.fosdem.model.StatusEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
class SearchViewModel(application: Application, private val state: SavedStateHandle) : AndroidViewModel(application) {
@HiltViewModel
class SearchViewModel @Inject constructor(scheduleDao: ScheduleDao, private val state: SavedStateHandle) : ViewModel() {
private val appDatabase = AppDatabase.getInstance(application)
private val queryLiveData: LiveData<String> = state.getLiveData(STATE_QUERY)
sealed class Result {
@ -26,9 +27,9 @@ class SearchViewModel(application: Application, private val state: SavedStateHan
if (query.length < SEARCH_QUERY_MIN_LENGTH) {
MutableLiveData(Result.QueryTooShort)
} else {
appDatabase.scheduleDao.getSearchResults(query)
.toLiveData(20)
.map { pagedList -> Result.Success(pagedList) }
scheduleDao.getSearchResults(query)
.toLiveData(20)
.map { pagedList -> Result.Success(pagedList) }
}
}

View file

@ -1,25 +1,25 @@
package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.db.ScheduleDao
import be.digitalia.fosdem.model.Day
import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.model.Track
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
class TrackScheduleEventViewModel(application: Application) : AndroidViewModel(application) {
@HiltViewModel
class TrackScheduleEventViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
private val appDatabase = AppDatabase.getInstance(application)
private val dayTrackLiveData = MutableLiveData<Pair<Day, Track>>()
val scheduleSnapshot: LiveData<List<Event>> = dayTrackLiveData.switchMap { (day, track) ->
MutableLiveData<List<Event>>().also {
appDatabase.queryExecutor.execute {
it.postValue(appDatabase.scheduleDao.getEventsSnapshot(day, track))
}
liveData {
emit(scheduleDao.getEventsSnapshot(day, track))
}
}

View file

@ -1,46 +1,47 @@
package be.digitalia.fosdem.viewmodels
import android.app.Application
import android.text.format.DateUtils
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import androidx.lifecycle.switchMap
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.db.ScheduleDao
import be.digitalia.fosdem.livedata.LiveDataFactory
import be.digitalia.fosdem.model.Day
import be.digitalia.fosdem.model.StatusEvent
import be.digitalia.fosdem.model.Track
import dagger.hilt.android.lifecycle.HiltViewModel
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class TrackScheduleListViewModel(application: Application) : AndroidViewModel(application) {
@HiltViewModel
class TrackScheduleListViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
private val appDatabase = AppDatabase.getInstance(application)
private val dayTrackLiveData = MutableLiveData<Pair<Day, Track>>()
val schedule: LiveData<List<StatusEvent>> = dayTrackLiveData.switchMap { (day, track) ->
appDatabase.scheduleDao.getEvents(day, track)
scheduleDao.getEvents(day, track)
}
/**
* @return The current time during the target day, or -1 outside of the target day.
*/
val currentTime: LiveData<Long> = dayTrackLiveData
.switchMap { (day, _) ->
// Auto refresh during the day passed as argument
val dayStart = day.date.time
LiveDataFactory.scheduler(dayStart, dayStart + DateUtils.DAY_IN_MILLIS)
}
.switchMap { isOn ->
if (isOn) {
LiveDataFactory.interval(REFRESH_TIME_INTERVAL, TimeUnit.MILLISECONDS).map {
System.currentTimeMillis()
}
} else {
MutableLiveData(-1L)
.switchMap { (day, _) ->
// Auto refresh during the day passed as argument
val dayStart = day.date.time
LiveDataFactory.scheduler(dayStart, dayStart + DateUtils.DAY_IN_MILLIS)
}
.switchMap { isOn ->
if (isOn) {
LiveDataFactory.interval(REFRESH_TIME_INTERVAL, TimeUnit.MILLISECONDS).map {
System.currentTimeMillis()
}
} else {
MutableLiveData(-1L)
}
}
fun setDayAndTrack(day: Day, track: Track) {
val dayTrack = day to track

View file

@ -1,21 +1,22 @@
package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap
import be.digitalia.fosdem.db.AppDatabase
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
class TracksViewModel(application: Application) : AndroidViewModel(application) {
@HiltViewModel
class TracksViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
private val appDatabase = AppDatabase.getInstance(application)
private val dayLiveData = MutableLiveData<Day>()
val tracks: LiveData<List<Track>> = dayLiveData.switchMap { day: Day ->
appDatabase.scheduleDao.getTracks(day)
scheduleDao.getTracks(day)
}
fun setDay(day: Day) {

View file

@ -1,6 +1,9 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.5.0'
ext {
kotlin_version = '1.5.0'
hilt_version = '2.35.1'
}
repositories {
google()
mavenCentral()
@ -8,6 +11,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:4.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}