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 ## Used libraries
* [Android Jetpack](https://developer.android.com/jetpack) by The Android Open Source Project * [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 * [Material Components for Android](https://material.io/develop/android) by The Android Open Source Project
* [OkHttp](https://github.com/square/okhttp) by Square, Inc. * [OkHttp](https://github.com/square/okhttp) by Square, Inc.
* [Moshi](https://github.com/square/moshi) by Square, Inc. * [Moshi](https://github.com/square/moshi) by Square, Inc.

View file

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

View file

@ -4,9 +4,15 @@ import android.app.Application
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import be.digitalia.fosdem.alarms.FosdemAlarmManager import be.digitalia.fosdem.alarms.FosdemAlarmManager
import be.digitalia.fosdem.utils.ThemeManager import be.digitalia.fosdem.utils.ThemeManager
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
@HiltAndroidApp
class FosdemApplication : Application() { class FosdemApplication : Application() {
@Inject
lateinit var alarmManager: FosdemAlarmManager
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -15,7 +21,7 @@ class FosdemApplication : Application() {
// Light/Dark theme switch (requires settings) // Light/Dark theme switch (requires settings)
ThemeManager.init(this) ThemeManager.init(this)
// Alarms (requires settings) // 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.BookmarkStatusViewModel
import be.digitalia.fosdem.viewmodels.EventViewModel import be.digitalia.fosdem.viewmodels.EventViewModel
import be.digitalia.fosdem.widgets.setupBookmarkStatus 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. * Displays a single event passed either as a complete Parcelable object in extras or as an id in data.
* *
* @author Christophe Beyls * @author Christophe Beyls
*/ */
@AndroidEntryPoint
class EventDetailsActivity : AppCompatActivity(R.layout.single_event), CreateNfcAppDataCallback { class EventDetailsActivity : AppCompatActivity(R.layout.single_event), CreateNfcAppDataCallback {
private val bookmarkStatusViewModel: BookmarkStatusViewModel by viewModels() 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.extractNfcAppData
import be.digitalia.fosdem.utils.hasNfcAppData import be.digitalia.fosdem.utils.hasNfcAppData
import be.digitalia.fosdem.utils.toBookmarks import be.digitalia.fosdem.utils.toBookmarks
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ExternalBookmarksActivity : SimpleToolbarActivity() { class ExternalBookmarksActivity : SimpleToolbarActivity() {
override fun onCreate(savedInstanceState: Bundle?) { 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.R
import be.digitalia.fosdem.api.FosdemApi import be.digitalia.fosdem.api.FosdemApi
import be.digitalia.fosdem.api.FosdemUrls 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.BookmarksListFragment
import be.digitalia.fosdem.fragments.LiveFragment import be.digitalia.fosdem.fragments.LiveFragment
import be.digitalia.fosdem.fragments.MapFragment 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.navigation.NavigationView
import com.google.android.material.progressindicator.BaseProgressIndicator import com.google.android.material.progressindicator.BaseProgressIndicator
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import javax.inject.Inject
/** /**
* Main entry point of the application. Allows to switch between section fragments and update the database. * Main entry point of the application. Allows to switch between section fragments and update the database.
* *
* @author Christophe Beyls * @author Christophe Beyls
*/ */
@AndroidEntryPoint
class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback { class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback {
private enum class Section(val fragmentClass: Class<out Fragment>, private enum class Section(val fragmentClass: Class<out Fragment>,
@ -79,6 +82,11 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
val drawerLayout: DrawerLayout, val drawerLayout: DrawerLayout,
val navigationView: NavigationView) val navigationView: NavigationView)
@Inject
lateinit var api: FosdemApi
@Inject
lateinit var scheduleDao: ScheduleDao
private lateinit var holder: ViewHolder private lateinit var holder: ViewHolder
private lateinit var drawerToggle: ActionBarDrawerToggle private lateinit var drawerToggle: ActionBarDrawerToggle
private var searchMenuItem: MenuItem? = null private var searchMenuItem: MenuItem? = null
@ -93,7 +101,7 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
val progressIndicator: BaseProgressIndicator<*> = findViewById(R.id.progress) val progressIndicator: BaseProgressIndicator<*> = findViewById(R.id.progress)
// Monitor the schedule download // Monitor the schedule download
FosdemApi.downloadScheduleState.observe(this) { state -> api.downloadScheduleState.observe(this) { state ->
when (state) { when (state) {
is LoadingState.Loading -> { is LoadingState.Loading -> {
with(progressIndicator) { with(progressIndicator) {
@ -119,7 +127,7 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
val snackbar = when (result) { val snackbar = when (result) {
is DownloadScheduleResult.Error -> { is DownloadScheduleResult.Error -> {
Snackbar.make(contentView, R.string.schedule_loading_error, ERROR_MESSAGE_DISPLAY_DURATION) 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 -> { is DownloadScheduleResult.UpToDate -> {
Snackbar.make(contentView, R.string.events_download_up_to_date, Snackbar.LENGTH_LONG) 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 // Latest update date, below the list
val latestUpdateTextView: TextView = navigationView.findViewById(R.id.latest_update) val latestUpdateTextView: TextView = navigationView.findViewById(R.id.latest_update)
AppDatabase.getInstance(this).scheduleDao.latestUpdateTime scheduleDao.latestUpdateTime
.observe(this) { time -> .observe(this) { time ->
val timeString = if (time == -1L) getString(R.string.never) val timeString = if (time == -1L) getString(R.string.never)
else DateFormat.format(LATEST_UPDATE_DATE_FORMAT, time) else DateFormat.format(LATEST_UPDATE_DATE_FORMAT, time)
@ -238,7 +246,7 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
// Scheduled database update // Scheduled database update
val now = System.currentTimeMillis() 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) { if (latestUpdateTime == null || latestUpdateTime < now - DATABASE_VALIDITY_DURATION) {
val prefs = getPreferences(Context.MODE_PRIVATE) val prefs = getPreferences(Context.MODE_PRIVATE)
val latestAttemptTime = prefs.getLong(PREF_LATEST_AUTO_UPDATE_ATTEMPT_TIME, -1L) 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) putLong(PREF_LATEST_AUTO_UPDATE_ATTEMPT_TIME, now)
} }
// Try to update immediately. If it fails, the user gets a message and a retry button. // Try to update immediately. If it fails, the user gets a message and a retry button.
FosdemApi.downloadSchedule(this) api.downloadSchedule()
} }
} }
} }
@ -289,7 +297,7 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
item.icon = icon item.icon = icon
icon.start() icon.start()
} }
FosdemApi.downloadSchedule(this) api.downloadSchedule()
true true
} }
else -> false 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.DateUtils
import be.digitalia.fosdem.utils.configureToolbarColors import be.digitalia.fosdem.utils.configureToolbarColors
import be.digitalia.fosdem.viewmodels.PersonInfoViewModel import be.digitalia.fosdem.viewmodels.PersonInfoViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class PersonInfoActivity : AppCompatActivity(R.layout.person_info) { class PersonInfoActivity : AppCompatActivity(R.layout.person_info) {
private val viewModel: PersonInfoViewModel by viewModels() 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.invertImageColors
import be.digitalia.fosdem.utils.isLightTheme import be.digitalia.fosdem.utils.isLightTheme
import be.digitalia.fosdem.utils.toSlug 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. * 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 * @author Christophe Beyls
*/ */
@AndroidEntryPoint
class RoomImageDialogActivity : AppCompatActivity(R.layout.dialog_room_image) { class RoomImageDialogActivity : AppCompatActivity(R.layout.dialog_room_image) {
@Inject
lateinit var api: FosdemApi
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val intent = intent val intent = intent
@ -40,14 +46,14 @@ class RoomImageDialogActivity : AppCompatActivity(R.layout.dialog_room_image) {
} }
setImageResource(intent.getIntExtra(EXTRA_ROOM_IMAGE_RESOURCE_ID, 0)) 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 { companion object {
const val EXTRA_ROOM_NAME = "roomName" const val EXTRA_ROOM_NAME = "roomName"
const val EXTRA_ROOM_IMAGE_RESOURCE_ID = "imageResId" 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 toolbar.title = roomName
if (roomName.isNotEmpty()) { if (roomName.isNotEmpty()) {
val context = toolbar.context val context = toolbar.context
@ -72,7 +78,7 @@ class RoomImageDialogActivity : AppCompatActivity(R.layout.dialog_room_image) {
} }
// Display the room status as subtitle // Display the room status as subtitle
FosdemApi.getRoomStatuses(toolbar.context).observe(owner) { roomStatuses -> api.roomStatuses.observe(owner) { roomStatuses ->
val roomStatus = roomStatuses[roomName] val roomStatus = roomStatuses[roomName]
toolbar.subtitle = if (roomStatus != null) { toolbar.subtitle = if (roomStatus != null) {
SpannableString(context.getString(roomStatus.nameResId)).apply { 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
import be.digitalia.fosdem.viewmodels.SearchViewModel.Result.QueryTooShort import be.digitalia.fosdem.viewmodels.SearchViewModel.Result.QueryTooShort
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class SearchResultActivity : SimpleToolbarActivity() { class SearchResultActivity : SimpleToolbarActivity() {
private val viewModel: SearchViewModel by viewModels() 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.BookmarkStatusViewModel
import be.digitalia.fosdem.viewmodels.TrackScheduleViewModel import be.digitalia.fosdem.viewmodels.TrackScheduleViewModel
import be.digitalia.fosdem.widgets.setupBookmarkStatus import be.digitalia.fosdem.widgets.setupBookmarkStatus
import dagger.hilt.android.AndroidEntryPoint
/** /**
* Track Schedule container, works in both single pane and dual pane modes. * Track Schedule container, works in both single pane and dual pane modes.
* *
* @author Christophe Beyls * @author Christophe Beyls
*/ */
@AndroidEntryPoint
class TrackScheduleActivity : AppCompatActivity(R.layout.track_schedule), CreateNfcAppDataCallback { class TrackScheduleActivity : AppCompatActivity(R.layout.track_schedule), CreateNfcAppDataCallback {
private val viewModel: TrackScheduleViewModel by viewModels() 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.viewmodels.TrackScheduleEventViewModel
import be.digitalia.fosdem.widgets.ContentLoadingViewMediator import be.digitalia.fosdem.widgets.ContentLoadingViewMediator
import be.digitalia.fosdem.widgets.setupBookmarkStatus 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. * Event view of the track schedule; allows to slide between events of the same track using a ViewPager.
* *
* @author Christophe Beyls * @author Christophe Beyls
*/ */
@AndroidEntryPoint
class TrackScheduleEventActivity : AppCompatActivity(R.layout.track_schedule_event), CreateNfcAppDataCallback { class TrackScheduleEventActivity : AppCompatActivity(R.layout.track_schedule_event), CreateNfcAppDataCallback {
private val bookmarkStatusViewModel: BookmarkStatusViewModel by viewModels() private val bookmarkStatusViewModel: BookmarkStatusViewModel by viewModels()

View file

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

View file

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

View file

@ -1,6 +1,5 @@
package be.digitalia.fosdem.api package be.digitalia.fosdem.api
import android.content.Context
import android.os.SystemClock import android.os.SystemClock
import android.text.format.DateUtils import android.text.format.DateUtils
import androidx.annotation.MainThread import androidx.annotation.MainThread
@ -9,7 +8,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.liveData import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap import androidx.lifecycle.switchMap
import be.digitalia.fosdem.alarms.FosdemAlarmManager 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.LiveDataFactory.scheduler
import be.digitalia.fosdem.livedata.SingleEvent import be.digitalia.fosdem.livedata.SingleEvent
import be.digitalia.fosdem.model.DownloadScheduleResult import be.digitalia.fosdem.model.DownloadScheduleResult
@ -26,6 +25,8 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okio.buffer import okio.buffer
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.pow import kotlin.math.pow
/** /**
@ -33,18 +34,13 @@ import kotlin.math.pow
* *
* @author Christophe Beyls * @author Christophe Beyls
*/ */
object FosdemApi { @Singleton
// 8:30 (local time) class FosdemApi @Inject constructor(
private const val DAY_START_TIME = 8 * DateUtils.HOUR_IN_MILLIS + 30 * DateUtils.MINUTE_IN_MILLIS private val scheduleDao: ScheduleDao,
// 19:00 (local time) private val alarmManager: FosdemAlarmManager
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
private var downloadJob: Job? = null private var downloadJob: Job? = null
private val _downloadScheduleState = MutableLiveData<LoadingState<DownloadScheduleResult>>() private val _downloadScheduleState = MutableLiveData<LoadingState<DownloadScheduleResult>>()
private var roomStatuses: LiveData<Map<String, RoomStatus>>? = null
/** /**
* Download & store the schedule to the database. * Download & store the schedule to the database.
@ -52,12 +48,11 @@ object FosdemApi {
* The result will be sent back through downloadScheduleResult LiveData. * The result will be sent back through downloadScheduleResult LiveData.
*/ */
@MainThread @MainThread
fun downloadSchedule(context: Context): Job { fun downloadSchedule(): Job {
// Returns the download job in progress, if any // Returns the download job in progress, if any
return downloadJob ?: run { return downloadJob ?: run {
val appContext = context.applicationContext
BackgroundWorkScope.launch { BackgroundWorkScope.launch {
downloadScheduleInternal(appContext) downloadScheduleInternal()
downloadJob = null downloadJob = null
}.also { }.also {
downloadJob = it downloadJob = it
@ -66,10 +61,9 @@ object FosdemApi {
} }
@MainThread @MainThread
private suspend fun downloadScheduleInternal(context: Context) { private suspend fun downloadScheduleInternal() {
_downloadScheduleState.value = LoadingState.Loading() _downloadScheduleState.value = LoadingState.Loading()
val res = try { val res = try {
val scheduleDao = AppDatabase.getInstance(context).scheduleDao
val response = HttpUtils.get(FosdemUrls.schedule, scheduleDao.lastModifiedTag) { body, rawResponse -> val response = HttpUtils.get(FosdemUrls.schedule, scheduleDao.lastModifiedTag) { body, rawResponse ->
val length = body.contentLength() val length = body.contentLength()
val source = if (length > 0L) { val source = if (length > 0L) {
@ -89,7 +83,7 @@ object FosdemApi {
when (response) { when (response) {
is HttpUtils.Response.NotModified -> DownloadScheduleResult.UpToDate // Nothing parsed, the result is up-to-date is HttpUtils.Response.NotModified -> DownloadScheduleResult.UpToDate // Nothing parsed, the result is up-to-date
is HttpUtils.Response.Success -> { is HttpUtils.Response.Success -> {
FosdemAlarmManager.onScheduleRefreshed() alarmManager.onScheduleRefreshed()
DownloadScheduleResult.Success(response.body) DownloadScheduleResult.Success(response.body)
} }
} }
@ -102,28 +96,24 @@ object FosdemApi {
val downloadScheduleState: LiveData<LoadingState<DownloadScheduleResult>> val downloadScheduleState: LiveData<LoadingState<DownloadScheduleResult>>
get() = _downloadScheduleState get() = _downloadScheduleState
@MainThread val roomStatuses: LiveData<Map<String, RoomStatus>> by lazy(LazyThreadSafetyMode.NONE) {
fun getRoomStatuses(context: Context): LiveData<Map<String, RoomStatus>> { // The room statuses will only be loaded when the event is live.
return roomStatuses ?: run { // Use the days from the database to determine it.
// The room statuses will only be loaded when the event is live. val scheduler = scheduleDao.days.switchMap { days ->
// Use the days from the database to determine it. val startEndTimestamps = LongArray(days.size * 2)
val scheduler = AppDatabase.getInstance(context).scheduleDao.days.switchMap { days -> var index = 0
val startEndTimestamps = LongArray(days.size * 2) for (day in days) {
var index = 0 val dayStart = day.date.time
for (day in days) { startEndTimestamps[index++] = dayStart + DAY_START_TIME
val dayStart = day.date.time startEndTimestamps[index++] = dayStart + DAY_END_TIME
startEndTimestamps[index++] = dayStart + DAY_START_TIME
startEndTimestamps[index++] = dayStart + DAY_END_TIME
}
scheduler(*startEndTimestamps)
} }
val liveRoomStatuses = buildLiveRoomStatusesLiveData() scheduler(*startEndTimestamps)
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 }
} }
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 package be.digitalia.fosdem.db
import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.room.Database import androidx.room.Database
import androidx.room.Room import androidx.room.DatabaseConfiguration
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import androidx.room.migration.Migration import be.digitalia.fosdem.alarms.FosdemAlarmManager
import androidx.sqlite.db.SupportSQLiteDatabase
import be.digitalia.fosdem.db.converters.GlobalTypeConverters import be.digitalia.fosdem.db.converters.GlobalTypeConverters
import be.digitalia.fosdem.db.entities.Bookmark import be.digitalia.fosdem.db.entities.Bookmark
import be.digitalia.fosdem.db.entities.EventEntity 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.Link
import be.digitalia.fosdem.model.Person import be.digitalia.fosdem.model.Person
import be.digitalia.fosdem.model.Track import be.digitalia.fosdem.model.Track
import 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, @Database(
Link::class, Track::class, Day::class, Bookmark::class], version = 2, exportSchema = false) entities = [EventEntity::class, EventTitles::class, Person::class, EventToPerson::class,
Link::class, Track::class, Day::class, Bookmark::class], version = 2, exportSchema = false
)
@TypeConverters(GlobalTypeConverters::class) @TypeConverters(GlobalTypeConverters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
lateinit var sharedPreferences: SharedPreferences
private set
abstract val scheduleDao: ScheduleDao abstract val scheduleDao: ScheduleDao
abstract val bookmarksDao: BookmarksDao abstract val bookmarksDao: BookmarksDao
companion object : SingletonHolder<AppDatabase, Context>({ context -> lateinit var sharedPreferences: SharedPreferences
val DB_FILE = "fosdem.sqlite" private set
val DB_PREFS_FILE = "database" lateinit var alarmManager: FosdemAlarmManager
val MIGRATION_1_2 = object : Migration(1, 2) { private set
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}")
}
}
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, DB_FILE) override fun init(configuration: DatabaseConfiguration) {
.addMigrations(MIGRATION_1_2) super.init(configuration)
.setJournalMode(JournalMode.TRUNCATE) // Manual dependency injection
.build().apply { val entryPoint = EntryPointAccessors.fromApplication(configuration.context, AppDatabaseEntryPoint::class.java)
sharedPreferences = context.applicationContext.getSharedPreferences(DB_PREFS_FILE, Context.MODE_PRIVATE) 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.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.withTransaction import androidx.room.withTransaction
import be.digitalia.fosdem.alarms.FosdemAlarmManager
import be.digitalia.fosdem.db.entities.Bookmark import be.digitalia.fosdem.db.entities.Bookmark
import be.digitalia.fosdem.model.AlarmInfo import be.digitalia.fosdem.model.AlarmInfo
import be.digitalia.fosdem.model.Event import be.digitalia.fosdem.model.Event
@ -16,7 +15,6 @@ import kotlinx.coroutines.launch
@Dao @Dao
abstract class BookmarksDao(private val appDatabase: AppDatabase) { abstract class BookmarksDao(private val appDatabase: AppDatabase) {
/** /**
* Returns the bookmarks. * Returns the bookmarks.
* *
@ -65,7 +63,7 @@ abstract class BookmarksDao(private val appDatabase: AppDatabase) {
BackgroundWorkScope.launch { BackgroundWorkScope.launch {
val ids = addBookmarksInternal(listOf(Bookmark(event.id))) val ids = addBookmarksInternal(listOf(Bookmark(event.id)))
if (ids[0] != -1L) { 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 // Filter out items that were already in bookmarks
val addedAlarmInfos = alarmInfos.filterIndexed { index, _ -> ids[index] != -1L } val addedAlarmInfos = alarmInfos.filterIndexed { index, _ -> ids[index] != -1L }
if (addedAlarmInfos.isNotEmpty()) { 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) { fun removeBookmarksAsync(eventIds: LongArray) {
BackgroundWorkScope.launch { BackgroundWorkScope.launch {
if (removeBookmarksInternal(eventIds) > 0) { 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` LEFT JOIN persons p ON ep.person_id = p.`rowid`
WHERE e.id = :id WHERE e.id = :id
GROUP BY e.id""") GROUP BY e.id""")
@WorkerThread abstract suspend fun getEvent(id: Long): Event?
abstract fun getEvent(id: Long): Event?
/** /**
* Returns all found events whose id is part of the given list. * 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 WHERE e.day_index = :day AND e.track_id = :track
GROUP BY e.id GROUP BY e.id
ORDER BY e.start_time ASC""") 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. * 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.R
import be.digitalia.fosdem.activities.ExternalBookmarksActivity import be.digitalia.fosdem.activities.ExternalBookmarksActivity
import be.digitalia.fosdem.adapters.BookmarksAdapter import be.digitalia.fosdem.adapters.BookmarksAdapter
import be.digitalia.fosdem.api.FosdemApi
import be.digitalia.fosdem.providers.BookmarksExportProvider import be.digitalia.fosdem.providers.BookmarksExportProvider
import be.digitalia.fosdem.utils.CreateNfcAppDataCallback import be.digitalia.fosdem.utils.CreateNfcAppDataCallback
import be.digitalia.fosdem.utils.toBookmarksNfcAppData import be.digitalia.fosdem.utils.toBookmarksNfcAppData
import be.digitalia.fosdem.viewmodels.BookmarksViewModel import be.digitalia.fosdem.viewmodels.BookmarksViewModel
import be.digitalia.fosdem.widgets.MultiChoiceHelper import be.digitalia.fosdem.widgets.MultiChoiceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import java.util.concurrent.CancellationException import java.util.concurrent.CancellationException
import javax.inject.Inject
/** /**
* Bookmarks list, optionally filterable. * Bookmarks list, optionally filterable.
* *
* @author Christophe Beyls * @author Christophe Beyls
*/ */
@AndroidEntryPoint
class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataCallback { class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataCallback {
@Inject
lateinit var api: FosdemApi
private val viewModel: BookmarksViewModel by viewModels() private val viewModel: BookmarksViewModel by viewModels()
private val multiChoiceHelper: MultiChoiceHelper by lazy(LazyThreadSafetyMode.NONE) { private val multiChoiceHelper: MultiChoiceHelper by lazy(LazyThreadSafetyMode.NONE) {
MultiChoiceHelper(requireActivity() as AppCompatActivity, this, object : MultiChoiceHelper.MultiChoiceModeListener { MultiChoiceHelper(requireActivity() as AppCompatActivity, this, object : MultiChoiceHelper.MultiChoiceModeListener {
@ -94,7 +100,7 @@ class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataC
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val adapter = BookmarksAdapter(view.context, viewLifecycleOwner, multiChoiceHelper) val adapter = BookmarksAdapter(view.context, multiChoiceHelper)
val holder = RecyclerViewViewHolder(view).apply { val holder = RecyclerViewViewHolder(view).apply {
recyclerView.apply { recyclerView.apply {
layoutManager = LinearLayoutManager(recyclerView.context) layoutManager = LinearLayoutManager(recyclerView.context)
@ -105,6 +111,9 @@ class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataC
isProgressBarVisible = true isProgressBarVisible = true
} }
api.roomStatuses.observe(viewLifecycleOwner) { statuses ->
adapter.roomStatuses = statuses
}
viewModel.bookmarks.observe(viewLifecycleOwner) { bookmarks -> viewModel.bookmarks.observe(viewLifecycleOwner) { bookmarks ->
adapter.submitList(bookmarks) adapter.submitList(bookmarks)
multiChoiceHelper.setAdapter(adapter, viewLifecycleOwner) 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.utils.stripHtml
import be.digitalia.fosdem.viewmodels.EventDetailsViewModel import be.digitalia.fosdem.viewmodels.EventDetailsViewModel
import com.google.android.material.snackbar.Snackbar 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) { class EventDetailsFragment : Fragment(R.layout.fragment_event_details) {
private class ViewHolder(view: View) { 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) val linksContainer: ViewGroup = view.findViewById(R.id.links_container)
} }
@Inject
lateinit var api: FosdemApi
private val viewModel: EventDetailsViewModel by viewModels() private val viewModel: EventDetailsViewModel by viewModels()
val event by lazy<Event>(LazyThreadSafetyMode.NONE) { val event by lazy<Event>(LazyThreadSafetyMode.NONE) {
@ -163,7 +168,7 @@ class EventDetailsFragment : Fragment(R.layout.fragment_event_details) {
val roomName = event.roomName val roomName = event.roomName
if (!roomName.isNullOrEmpty()) { if (!roomName.isNullOrEmpty()) {
holder.roomStatusTextView.run { holder.roomStatusTextView.run {
FosdemApi.getRoomStatuses(context).observe(viewLifecycleOwner) { roomStatuses -> api.roomStatuses.observe(viewLifecycleOwner) { roomStatuses ->
val roomStatus = roomStatuses[roomName] val roomStatus = roomStatuses[roomName]
if (roomStatus == null) { if (roomStatus == null) {
text = null text = null

View file

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

View file

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

View file

@ -11,19 +11,25 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import be.digitalia.fosdem.R import be.digitalia.fosdem.R
import be.digitalia.fosdem.adapters.EventsAdapter import be.digitalia.fosdem.adapters.EventsAdapter
import be.digitalia.fosdem.api.FosdemApi
import be.digitalia.fosdem.model.StatusEvent import be.digitalia.fosdem.model.StatusEvent
import be.digitalia.fosdem.viewmodels.LiveViewModel import be.digitalia.fosdem.viewmodels.LiveViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
sealed class LiveListFragment(@StringRes private val emptyTextResId: Int, sealed class LiveListFragment(@StringRes private val emptyTextResId: Int,
private val dataSourceProvider: (LiveViewModel) -> LiveData<PagedList<StatusEvent>>) private val dataSourceProvider: (LiveViewModel) -> LiveData<PagedList<StatusEvent>>)
: Fragment(R.layout.recyclerview) { : Fragment(R.layout.recyclerview) {
@Inject
lateinit var api: FosdemApi
private val viewModel: LiveViewModel by viewModels({ requireParentFragment() }) private val viewModel: LiveViewModel by viewModels({ requireParentFragment() })
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val adapter = EventsAdapter(view.context, viewLifecycleOwner, false) val adapter = EventsAdapter(view.context, false)
val holder = RecyclerViewViewHolder(view).apply { val holder = RecyclerViewViewHolder(view).apply {
recyclerView.apply { recyclerView.apply {
val parent = parentFragment val parent = parentFragment
@ -39,6 +45,9 @@ sealed class LiveListFragment(@StringRes private val emptyTextResId: Int,
isProgressBarVisible = true isProgressBarVisible = true
} }
api.roomStatuses.observe(viewLifecycleOwner) { statuses ->
adapter.roomStatuses = statuses
}
dataSourceProvider(viewModel).observe(viewLifecycleOwner) { events -> dataSourceProvider(viewModel).observe(viewLifecycleOwner) { events ->
adapter.submitList(events) { adapter.submitList(events) {
// Ensure we stay at scroll position 0 so we can see the insertion animation // 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.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import be.digitalia.fosdem.R import be.digitalia.fosdem.R
import be.digitalia.fosdem.adapters.EventsAdapter import be.digitalia.fosdem.adapters.EventsAdapter
import be.digitalia.fosdem.api.FosdemApi
import be.digitalia.fosdem.viewmodels.PersonInfoViewModel import be.digitalia.fosdem.viewmodels.PersonInfoViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class PersonInfoListFragment : Fragment(R.layout.recyclerview) { class PersonInfoListFragment : Fragment(R.layout.recyclerview) {
@Inject
lateinit var api: FosdemApi
// Fetch data from parent Activity's ViewModel // 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val adapter = EventsAdapter(view.context, viewLifecycleOwner) val adapter = EventsAdapter(view.context)
val holder = RecyclerViewViewHolder(view).apply { val holder = RecyclerViewViewHolder(view).apply {
recyclerView.apply { recyclerView.apply {
val contentMargin = resources.getDimensionPixelSize(R.dimen.content_margin) val contentMargin = resources.getDimensionPixelSize(R.dimen.content_margin)
@ -37,6 +44,9 @@ class PersonInfoListFragment : Fragment(R.layout.recyclerview) {
isProgressBarVisible = true isProgressBarVisible = true
} }
api.roomStatuses.observe(viewLifecycleOwner) { statuses ->
adapter.roomStatuses = statuses
}
viewModel.events.observe(viewLifecycleOwner) { events -> viewModel.events.observe(viewLifecycleOwner) { events ->
adapter.submitList(events) adapter.submitList(events)
holder.isProgressBarVisible = false 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.adapters.createSimpleItemCallback
import be.digitalia.fosdem.model.Person import be.digitalia.fosdem.model.Person
import be.digitalia.fosdem.viewmodels.PersonsViewModel import be.digitalia.fosdem.viewmodels.PersonsViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class PersonsListFragment : Fragment(R.layout.recyclerview_fastscroll) { class PersonsListFragment : Fragment(R.layout.recyclerview_fastscroll) {
private val viewModel: PersonsViewModel by viewModels() private val viewModel: PersonsViewModel by viewModels()

View file

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

View file

@ -8,16 +8,22 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import be.digitalia.fosdem.R import be.digitalia.fosdem.R
import be.digitalia.fosdem.adapters.EventsAdapter import be.digitalia.fosdem.adapters.EventsAdapter
import be.digitalia.fosdem.api.FosdemApi
import be.digitalia.fosdem.viewmodels.SearchViewModel import be.digitalia.fosdem.viewmodels.SearchViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class SearchResultListFragment : Fragment(R.layout.recyclerview) { class SearchResultListFragment : Fragment(R.layout.recyclerview) {
@Inject
lateinit var api: FosdemApi
private val viewModel: SearchViewModel by activityViewModels() private val viewModel: SearchViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val adapter = EventsAdapter(view.context, viewLifecycleOwner) val adapter = EventsAdapter(view.context)
val holder = RecyclerViewViewHolder(view).apply { val holder = RecyclerViewViewHolder(view).apply {
recyclerView.apply { recyclerView.apply {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
@ -28,6 +34,9 @@ class SearchResultListFragment : Fragment(R.layout.recyclerview) {
isProgressBarVisible = true isProgressBarVisible = true
} }
api.roomStatuses.observe(viewLifecycleOwner) { statuses ->
adapter.roomStatuses = statuses
}
viewModel.results.observe(viewLifecycleOwner) { result -> viewModel.results.observe(viewLifecycleOwner) { result ->
adapter.submitList((result as? SearchViewModel.Result.Success)?.list) adapter.submitList((result as? SearchViewModel.Result.Success)?.list)
holder.isProgressBarVisible = false 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.model.Track
import be.digitalia.fosdem.viewmodels.TrackScheduleListViewModel import be.digitalia.fosdem.viewmodels.TrackScheduleListViewModel
import be.digitalia.fosdem.viewmodels.TrackScheduleViewModel import be.digitalia.fosdem.viewmodels.TrackScheduleViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class TrackScheduleListFragment : Fragment(R.layout.recyclerview), TrackScheduleAdapter.EventClickListener { class TrackScheduleListFragment : Fragment(R.layout.recyclerview), TrackScheduleAdapter.EventClickListener {
private val viewModel: TrackScheduleListViewModel by viewModels() 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.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import be.digitalia.fosdem.R import be.digitalia.fosdem.R
import be.digitalia.fosdem.db.AppDatabase import be.digitalia.fosdem.db.ScheduleDao
import be.digitalia.fosdem.model.Day import be.digitalia.fosdem.model.Day
import be.digitalia.fosdem.utils.enforceSingleScrollDirection import be.digitalia.fosdem.utils.enforceSingleScrollDirection
import be.digitalia.fosdem.utils.instantiate import be.digitalia.fosdem.utils.instantiate
@ -20,7 +20,10 @@ import be.digitalia.fosdem.utils.recyclerView
import be.digitalia.fosdem.utils.viewLifecycleLazy import be.digitalia.fosdem.utils.viewLifecycleLazy
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvider { class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvider {
private class ViewHolder(view: View) { 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) val tabs: TabLayout = view.findViewById(R.id.tabs)
} }
@Inject
lateinit var scheduleDao: ScheduleDao
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) 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) requireActivity().getPreferences(Context.MODE_PRIVATE).getInt(PREF_CURRENT_PAGE, -1)
} else -1 } else -1
AppDatabase.getInstance(requireContext()).scheduleDao.days.observe(viewLifecycleOwner) { days -> scheduleDao.days.observe(viewLifecycleOwner) { days ->
holder.run { holder.run {
daysAdapter.days = days 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.Day
import be.digitalia.fosdem.model.Track import be.digitalia.fosdem.model.Track
import be.digitalia.fosdem.viewmodels.TracksViewModel import be.digitalia.fosdem.viewmodels.TracksViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class TracksListFragment : Fragment(R.layout.recyclerview) { class TracksListFragment : Fragment(R.layout.recyclerview) {
private val viewModel: TracksViewModel by viewModels() 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.os.ParcelFileDescriptor
import android.provider.OpenableColumns import android.provider.OpenableColumns
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.core.content.ContentProviderCompat
import be.digitalia.fosdem.BuildConfig import be.digitalia.fosdem.BuildConfig
import be.digitalia.fosdem.R import be.digitalia.fosdem.R
import be.digitalia.fosdem.api.FosdemUrls 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.ical.ICalendarWriter
import be.digitalia.fosdem.model.Event import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.utils.DateUtils import be.digitalia.fosdem.utils.DateUtils
import be.digitalia.fosdem.utils.stripHtml import be.digitalia.fosdem.utils.stripHtml
import be.digitalia.fosdem.utils.toSlug 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.buffer
import okio.sink import okio.sink
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -34,6 +40,19 @@ import java.util.TimeZone
*/ */
class BookmarksExportProvider : ContentProvider() { 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 onCreate() = true
override fun insert(uri: Uri, values: ContentValues?) = throw UnsupportedOperationException() override fun insert(uri: Uri, values: ContentValues?) = throw UnsupportedOperationException()
@ -45,7 +64,7 @@ class BookmarksExportProvider : ContentProvider() {
override fun getType(uri: Uri) = TYPE override fun getType(uri: Uri) = TYPE
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? { 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 proj = projection ?: COLUMNS
val cols = arrayOfNulls<String>(proj.size) val cols = arrayOfNulls<String>(proj.size)
val values = arrayOfNulls<Any>(proj.size) val values = arrayOfNulls<Any>(proj.size)
@ -54,7 +73,7 @@ class BookmarksExportProvider : ContentProvider() {
when (col) { when (col) {
OpenableColumns.DISPLAY_NAME -> { OpenableColumns.DISPLAY_NAME -> {
cols[columnCount] = OpenableColumns.DISPLAY_NAME cols[columnCount] = OpenableColumns.DISPLAY_NAME
values[columnCount++] = ctx.getString(R.string.export_bookmarks_file_name, AppDatabase.getInstance(ctx).scheduleDao.getYear()) values[columnCount++] = ctx.getString(R.string.export_bookmarks_file_name, scheduleDao.getYear())
} }
OpenableColumns.SIZE -> { OpenableColumns.SIZE -> {
cols[columnCount] = OpenableColumns.SIZE cols[columnCount] = OpenableColumns.SIZE
@ -72,17 +91,14 @@ class BookmarksExportProvider : ContentProvider() {
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
return try { return try {
val pipe = ParcelFileDescriptor.createPipe() val pipe = ParcelFileDescriptor.createPipe()
DownloadThread( DownloadThread(ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]), bookmarksDao).start()
ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]),
AppDatabase.getInstance(context!!)
).start()
pipe[0] pipe[0]
} catch (e: IOException) { } catch (e: IOException) {
throw FileNotFoundException("Could not open pipe") 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) private val calendar = Calendar.getInstance(DateUtils.belgiumTimeZone, Locale.US)
// Format all times in GMT // Format all times in GMT
private val dateFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US).apply { private val dateFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US).apply {
@ -93,7 +109,7 @@ class BookmarksExportProvider : ContentProvider() {
override fun run() { override fun run() {
try { try {
ICalendarWriter(outputStream.sink().buffer()).use { writer -> ICalendarWriter(outputStream.sink().buffer()).use { writer ->
val bookmarks = appDatabase.bookmarksDao.getBookmarks() val bookmarks = bookmarksDao.getBookmarks()
writer.write("BEGIN", "VCALENDAR") writer.write("BEGIN", "VCALENDAR")
writer.write("VERSION", "2.0") writer.write("VERSION", "2.0")
writer.write("PRODID", "-//${BuildConfig.APPLICATION_ID}//NONSGML ${BuildConfig.VERSION_NAME}//EN") 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 { companion object {
const val TYPE = "text/calendar" const val TYPE = "text/calendar"
private val URI = Uri.Builder() private val URI = Uri.Builder()

View file

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

View file

@ -6,23 +6,29 @@ import android.content.Intent
import be.digitalia.fosdem.BuildConfig import be.digitalia.fosdem.BuildConfig
import be.digitalia.fosdem.alarms.FosdemAlarmManager import be.digitalia.fosdem.alarms.FosdemAlarmManager
import be.digitalia.fosdem.services.AlarmIntentService 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. * Entry point for system-generated events: boot complete and alarms.
* *
* @author Christophe Beyls * @author Christophe Beyls
*/ */
@AndroidEntryPoint
class AlarmReceiver : BroadcastReceiver() { class AlarmReceiver : BroadcastReceiver() {
@Inject
lateinit var alarmManager: FosdemAlarmManager
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
when (intent.action) { when (intent.action) {
ACTION_NOTIFY_EVENT -> { ACTION_NOTIFY_EVENT -> {
val serviceIntent = Intent(ACTION_NOTIFY_EVENT) val serviceIntent = Intent(ACTION_NOTIFY_EVENT)
.setData(intent.data) .setData(intent.data)
AlarmIntentService.enqueueWork(context, serviceIntent) AlarmIntentService.enqueueWork(context, serviceIntent)
} }
Intent.ACTION_BOOT_COMPLETED -> { 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 else AlarmIntentService.ACTION_DISABLE_ALARMS
val serviceIntent = Intent(serviceAction) val serviceIntent = Intent(serviceAction)
AlarmIntentService.enqueueWork(context, serviceIntent) 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.EventDetailsActivity
import be.digitalia.fosdem.activities.MainActivity import be.digitalia.fosdem.activities.MainActivity
import be.digitalia.fosdem.activities.RoomImageDialogActivity 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.AlarmInfo
import be.digitalia.fosdem.model.Event import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.receivers.AlarmReceiver import be.digitalia.fosdem.receivers.AlarmReceiver
import be.digitalia.fosdem.utils.PreferenceKeys import be.digitalia.fosdem.utils.PreferenceKeys
import be.digitalia.fosdem.utils.roomNameToResourceName 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. * A service to schedule or unschedule alarms in the background, keeping the app responsive.
* *
* @author Christophe Beyls * @author Christophe Beyls
*/ */
@AndroidEntryPoint
class AlarmIntentService : JobIntentService() { class AlarmIntentService : JobIntentService() {
@Inject
lateinit var bookmarksDao: BookmarksDao
@Inject
lateinit var scheduleDao: ScheduleDao
private val alarmManager by lazy<AlarmManager> { private val alarmManager by lazy<AlarmManager> {
getSystemService()!! getSystemService()!!
} }
@ -63,7 +73,7 @@ class AlarmIntentService : JobIntentService() {
val delay = delay val delay = delay
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
var hasAlarms = false var hasAlarms = false
for (info in AppDatabase.getInstance(this).bookmarksDao.getBookmarksAlarmInfo(0L)) { for (info in bookmarksDao.getBookmarksAlarmInfo(0L)) {
val startTime = info.startTime val startTime = info.startTime
val notificationTime = if (startTime == null) -1L else startTime.time - delay val notificationTime = if (startTime == null) -1L else startTime.time - delay
val pi = getAlarmPendingIntent(info.eventId) val pi = getAlarmPendingIntent(info.eventId)
@ -82,7 +92,7 @@ class AlarmIntentService : JobIntentService() {
} }
ACTION_DISABLE_ALARMS -> { ACTION_DISABLE_ALARMS -> {
// Cancel alarms of every bookmark in the future // 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)) alarmManager.cancel(getAlarmPendingIntent(info.eventId))
} }
setAlarmReceiverEnabled(false) setAlarmReceiverEnabled(false)
@ -116,7 +126,7 @@ class AlarmIntentService : JobIntentService() {
} }
AlarmReceiver.ACTION_NOTIFY_EVENT -> { AlarmReceiver.ACTION_NOTIFY_EVENT -> {
val eventId = intent.dataString!!.toLong() val eventId = intent.dataString!!.toLong()
val event = AppDatabase.getInstance(this).scheduleDao.getEvent(eventId) val event = runBlocking { scheduleDao.getEvent(eventId) }
if (event != null) { if (event != null) {
NotificationManagerCompat.from(this).notify(eventId.toInt(), buildNotification(event)) 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 package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.lifecycle.switchMap 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.BookmarkStatus
import be.digitalia.fosdem.model.Event 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 val eventLiveData = MutableLiveData<Event?>()
private var firstResultReceived = false private var firstResultReceived = false
@ -21,13 +22,13 @@ class BookmarkStatusViewModel(application: Application) : AndroidViewModel(appli
if (event == null) { if (event == null) {
MutableLiveData(null) MutableLiveData(null)
} else { } else {
appDatabase.bookmarksDao.getBookmarkStatus(event) bookmarksDao.getBookmarkStatus(event)
.distinctUntilChanged() // Prevent updating the UI when a bookmark is added back or removed back .distinctUntilChanged() // Prevent updating the UI when a bookmark is added back or removed back
.map { isBookmarked -> .map { isBookmarked ->
val isUpdate = firstResultReceived val isUpdate = firstResultReceived
firstResultReceived = true firstResultReceived = true
BookmarkStatus(isBookmarked, isUpdate) 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 // Ignore the action if the status for the current event hasn't been received yet
if (event != null && currentStatus != null && firstResultReceived) { if (event != null && currentStatus != null && firstResultReceived) {
if (currentStatus.isBookmarked) { if (currentStatus.isBookmarked) {
appDatabase.bookmarksDao.removeBookmarkAsync(event) bookmarksDao.removeBookmarkAsync(event)
} else { } 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.app.Application
import android.net.Uri import android.net.Uri
import android.text.format.DateUtils import android.text.format.DateUtils
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap import androidx.lifecycle.switchMap
import be.digitalia.fosdem.BuildConfig 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.livedata.LiveDataFactory
import be.digitalia.fosdem.model.Event import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.parsers.ExportedBookmarksParser import be.digitalia.fosdem.parsers.ExportedBookmarksParser
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okio.buffer import okio.buffer
import okio.source import okio.source
import java.util.concurrent.TimeUnit 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>() private val upcomingOnlyLiveData = MutableLiveData<Boolean>()
val bookmarks: LiveData<List<Event>> = upcomingOnlyLiveData.switchMap { upcomingOnly: Boolean -> val bookmarks: LiveData<List<Event>> = upcomingOnlyLiveData.switchMap { upcomingOnly: Boolean ->
if (upcomingOnly) { if (upcomingOnly) {
// Refresh upcoming bookmarks every 2 minutes // Refresh upcoming bookmarks every 2 minutes
LiveDataFactory.interval(2L, TimeUnit.MINUTES) LiveDataFactory.interval(2L, TimeUnit.MINUTES)
.switchMap { .switchMap {
appDatabase.bookmarksDao.getBookmarks(System.currentTimeMillis() - TIME_OFFSET) bookmarksDao.getBookmarks(System.currentTimeMillis() - TIME_OFFSET)
} }
} else { } else {
appDatabase.bookmarksDao.getBookmarks(-1L) bookmarksDao.getBookmarks(-1L)
} }
} }
@ -44,15 +51,14 @@ class BookmarksViewModel(application: Application) : AndroidViewModel(applicatio
} }
fun removeBookmarks(eventIds: LongArray) { fun removeBookmarks(eventIds: LongArray) {
appDatabase.bookmarksDao.removeBookmarksAsync(eventIds) bookmarksDao.removeBookmarksAsync(eventIds)
} }
suspend fun readBookmarkIds(uri: Uri): LongArray { @Suppress("BlockingMethodInNonBlockingContext")
return withContext(Dispatchers.IO) { suspend fun readBookmarkIds(uri: Uri): LongArray = withContext(Dispatchers.IO) {
val parser = ExportedBookmarksParser(BuildConfig.APPLICATION_ID, appDatabase.scheduleDao.getYear()) val parser = ExportedBookmarksParser(BuildConfig.APPLICATION_ID, scheduleDao.getYear())
checkNotNull(getApplication<Application>().contentResolver.openInputStream(uri)).source().buffer().use { checkNotNull(application.contentResolver.openInputStream(uri)).source().buffer().use {
parser.parse(it) parser.parse(it)
}
} }
} }

View file

@ -1,21 +1,22 @@
package be.digitalia.fosdem.viewmodels package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap 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.Event
import be.digitalia.fosdem.model.EventDetails 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>() private val eventLiveData = MutableLiveData<Event>()
val eventDetails: LiveData<EventDetails> = eventLiveData.switchMap { event: Event -> val eventDetails: LiveData<EventDetails> = eventLiveData.switchMap { event: Event ->
appDatabase.scheduleDao.getEventDetails(event) scheduleDao.getEventDetails(event)
} }
fun setEvent(event: Event) { fun setEvent(event: Event) {

View file

@ -1,27 +1,28 @@
package be.digitalia.fosdem.viewmodels package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap 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.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>() private val eventIdLiveData = MutableLiveData<Long>()
val event: LiveData<Event?> = eventIdLiveData.switchMap { id: Long -> val event: LiveData<Event?> = eventIdLiveData.switchMap { id ->
MutableLiveData<Event?>().also { liveData {
appDatabase.queryExecutor.execute { emit(scheduleDao.getEvent(id))
it.postValue(appDatabase.scheduleDao.getEvent(id))
}
} }
} }
val isEventIdSet = eventIdLiveData.value != null val isEventIdSet
get() = eventIdLiveData.value != null
fun setEventId(eventId: Long) { fun setEventId(eventId: Long) {
if (eventId != eventIdLiveData.value) { if (eventId != eventIdLiveData.value) {

View file

@ -1,22 +1,27 @@
package be.digitalia.fosdem.viewmodels package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap import androidx.lifecycle.switchMap
import androidx.paging.PagedList import androidx.paging.PagedList
import androidx.paging.toLiveData 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 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>() private val bookmarkIdsLiveData = MutableLiveData<LongArray>()
val bookmarks: LiveData<PagedList<StatusEvent>> = bookmarkIdsLiveData.switchMap { bookmarkIds: LongArray -> val bookmarks: LiveData<PagedList<StatusEvent>> = bookmarkIdsLiveData.switchMap { bookmarkIds ->
appDatabase.scheduleDao.getEvents(bookmarkIds).toLiveData(20) scheduleDao.getEvents(bookmarkIds).toLiveData(20)
} }
fun setBookmarkIds(bookmarkIds: LongArray) { fun setBookmarkIds(bookmarkIds: LongArray) {
@ -28,6 +33,6 @@ class ExternalBookmarksViewModel(application: Application) : AndroidViewModel(ap
fun addAll() { fun addAll() {
val bookmarkIds = bookmarkIdsLiveData.value ?: return val bookmarkIds = bookmarkIdsLiveData.value ?: return
appDatabase.bookmarksDao.addBookmarksAsync(bookmarkIds) bookmarksDao.addBookmarksAsync(bookmarkIds)
} }
} }

View file

@ -1,29 +1,30 @@
package be.digitalia.fosdem.viewmodels package be.digitalia.fosdem.viewmodels
import android.app.Application
import android.text.format.DateUtils import android.text.format.DateUtils
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap import androidx.lifecycle.switchMap
import androidx.paging.PagedList import androidx.paging.PagedList
import androidx.paging.toLiveData 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.livedata.LiveDataFactory
import be.digitalia.fosdem.model.StatusEvent import be.digitalia.fosdem.model.StatusEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import java.util.concurrent.TimeUnit 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) private val heartbeat = LiveDataFactory.interval(1L, TimeUnit.MINUTES)
val nextEvents: LiveData<PagedList<StatusEvent>> = heartbeat.switchMap { val nextEvents: LiveData<PagedList<StatusEvent>> = heartbeat.switchMap {
val now = System.currentTimeMillis() 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 { val eventsInProgress: LiveData<PagedList<StatusEvent>> = heartbeat.switchMap {
appDatabase.scheduleDao.getEventsInProgress(System.currentTimeMillis()).toLiveData(20) scheduleDao.getEventsInProgress(System.currentTimeMillis()).toLiveData(20)
} }
companion object { companion object {

View file

@ -1,23 +1,24 @@
package be.digitalia.fosdem.viewmodels package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap import androidx.lifecycle.switchMap
import androidx.paging.PagedList import androidx.paging.PagedList
import androidx.paging.toLiveData 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.Person
import be.digitalia.fosdem.model.StatusEvent 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>() private val personLiveData = MutableLiveData<Person>()
val events: LiveData<PagedList<StatusEvent>> = personLiveData.switchMap { person: 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) { fun setPerson(person: Person) {

View file

@ -1,16 +1,16 @@
package be.digitalia.fosdem.viewmodels package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.paging.PagedList import androidx.paging.PagedList
import androidx.paging.toLiveData 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.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>> = scheduleDao.getPersons().toLiveData(100)
val persons: LiveData<PagedList<Person>> = appDatabase.scheduleDao.getPersons().toLiveData(100)
} }

View file

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

View file

@ -1,25 +1,25 @@
package be.digitalia.fosdem.viewmodels package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap 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.Day
import be.digitalia.fosdem.model.Event import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.model.Track 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>>() private val dayTrackLiveData = MutableLiveData<Pair<Day, Track>>()
val scheduleSnapshot: LiveData<List<Event>> = dayTrackLiveData.switchMap { (day, track) -> val scheduleSnapshot: LiveData<List<Event>> = dayTrackLiveData.switchMap { (day, track) ->
MutableLiveData<List<Event>>().also { liveData {
appDatabase.queryExecutor.execute { emit(scheduleDao.getEventsSnapshot(day, track))
it.postValue(appDatabase.scheduleDao.getEventsSnapshot(day, track))
}
} }
} }

View file

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

View file

@ -1,21 +1,22 @@
package be.digitalia.fosdem.viewmodels package be.digitalia.fosdem.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap 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.Day
import be.digitalia.fosdem.model.Track 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>() private val dayLiveData = MutableLiveData<Day>()
val tracks: LiveData<List<Track>> = dayLiveData.switchMap { day: Day -> val tracks: LiveData<List<Track>> = dayLiveData.switchMap { day: Day ->
appDatabase.scheduleDao.getTracks(day) scheduleDao.getTracks(day)
} }
fun setDay(day: 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. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.5.0' ext {
kotlin_version = '1.5.0'
hilt_version = '2.35.1'
}
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
@ -8,6 +11,7 @@ buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.2.1' classpath 'com.android.tools.build:gradle:4.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
} }
} }