mirror of
https://github.com/MatomoCamp/matomocamp-companion-android.git
synced 2024-09-19 16:13:46 +02:00
Migrate from LiveData to Kotlin Flow (#77)
- Replace all usages of LiveData with Flow and StateFlow - Replace Paging 2 with Flow-based Paging 3 - Inject custom ViewModel arguments directly through custom ViewModel Factories.
This commit is contained in:
parent
2b96e266cd
commit
3f6c9d6219
47 changed files with 1041 additions and 748 deletions
|
@ -118,9 +118,9 @@ dependencies {
|
||||||
implementation 'androidx.browser:browser:1.4.0'
|
implementation 'androidx.browser:browser:1.4.0'
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
implementation 'androidx.paging:paging-runtime:3.1.0'
|
||||||
implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
|
|
||||||
implementation "androidx.room:room-ktx:$room_version"
|
implementation "androidx.room:room-ktx:$room_version"
|
||||||
|
implementation "androidx.room:room-paging:$room_version"
|
||||||
implementation "androidx.datastore:datastore-preferences:1.0.0"
|
implementation "androidx.datastore:datastore-preferences:1.0.0"
|
||||||
kapt "androidx.room:room-compiler:$room_version"
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||||
|
|
|
@ -12,10 +12,12 @@ import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.add
|
import androidx.fragment.app.add
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import be.digitalia.fosdem.R
|
import be.digitalia.fosdem.R
|
||||||
import be.digitalia.fosdem.fragments.EventDetailsFragment
|
import be.digitalia.fosdem.fragments.EventDetailsFragment
|
||||||
import be.digitalia.fosdem.model.Event
|
import be.digitalia.fosdem.model.Event
|
||||||
import be.digitalia.fosdem.utils.CreateNfcAppDataCallback
|
import be.digitalia.fosdem.utils.CreateNfcAppDataCallback
|
||||||
|
import be.digitalia.fosdem.utils.assistedViewModels
|
||||||
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.isLightTheme
|
import be.digitalia.fosdem.utils.isLightTheme
|
||||||
|
@ -29,6 +31,7 @@ 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
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
|
@ -39,7 +42,20 @@ import dagger.hilt.android.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()
|
||||||
private val viewModel: EventViewModel by viewModels()
|
@Inject
|
||||||
|
lateinit var viewModelFactory: EventViewModel.Factory
|
||||||
|
private val viewModel: EventViewModel by assistedViewModels {
|
||||||
|
// Load the event from the DB using its id
|
||||||
|
val intent = intent
|
||||||
|
val eventIdString = if (intent.hasNfcAppData()) {
|
||||||
|
// NFC intent
|
||||||
|
intent.extractNfcAppData().toEventIdString()
|
||||||
|
} else {
|
||||||
|
// Normal in-app intent
|
||||||
|
intent.dataString!!
|
||||||
|
}
|
||||||
|
viewModelFactory.create(eventIdString.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -59,23 +75,11 @@ class EventDetailsActivity : AppCompatActivity(R.layout.single_event), CreateNfc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Load the event from the DB using its id
|
lifecycleScope.launchWhenStarted {
|
||||||
if (!viewModel.isEventIdSet) {
|
val event = viewModel.event.await()
|
||||||
val intent = intent
|
|
||||||
val eventIdString = if (intent.hasNfcAppData()) {
|
|
||||||
// NFC intent
|
|
||||||
intent.extractNfcAppData().toEventIdString()
|
|
||||||
} else {
|
|
||||||
// Normal in-app intent
|
|
||||||
intent.dataString!!
|
|
||||||
}
|
|
||||||
viewModel.setEventId(eventIdString.toLong())
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.event.observe(this) { event ->
|
|
||||||
if (event == null) {
|
if (event == null) {
|
||||||
// Event not found, quit
|
// Event not found, quit
|
||||||
Toast.makeText(this, getString(R.string.event_not_found_error), Toast.LENGTH_LONG).show()
|
Toast.makeText(this@EventDetailsActivity, getString(R.string.event_not_found_error), Toast.LENGTH_LONG).show()
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
initEvent(event)
|
initEvent(event)
|
||||||
|
|
|
@ -38,6 +38,7 @@ import be.digitalia.fosdem.model.LoadingState
|
||||||
import be.digitalia.fosdem.utils.CreateNfcAppDataCallback
|
import be.digitalia.fosdem.utils.CreateNfcAppDataCallback
|
||||||
import be.digitalia.fosdem.utils.awaitCloseDrawer
|
import be.digitalia.fosdem.utils.awaitCloseDrawer
|
||||||
import be.digitalia.fosdem.utils.configureToolbarColors
|
import be.digitalia.fosdem.utils.configureToolbarColors
|
||||||
|
import be.digitalia.fosdem.utils.launchAndRepeatOnLifecycle
|
||||||
import be.digitalia.fosdem.utils.setNfcAppDataPushMessageCallbackIfAvailable
|
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
|
||||||
|
@ -107,7 +108,8 @@ 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
|
||||||
api.downloadScheduleState.observe(this) { state ->
|
launchAndRepeatOnLifecycle {
|
||||||
|
api.downloadScheduleState.collect { state ->
|
||||||
when (state) {
|
when (state) {
|
||||||
is LoadingState.Loading -> {
|
is LoadingState.Loading -> {
|
||||||
with(progressIndicator) {
|
with(progressIndicator) {
|
||||||
|
@ -129,7 +131,7 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
state.result.consume()?.let { result ->
|
state.result?.let { result ->
|
||||||
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)
|
||||||
|
@ -148,7 +150,12 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
|
||||||
Snackbar.make(contentView, message, Snackbar.LENGTH_LONG)
|
Snackbar.make(contentView, message, Snackbar.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
snackbar.show()
|
snackbar.addCallback(object : Snackbar.Callback() {
|
||||||
|
override fun onDismissed(transientBottomBar: Snackbar, event: Int) {
|
||||||
|
api.downloadScheduleResultConsumed()
|
||||||
|
}
|
||||||
|
}).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import be.digitalia.fosdem.api.FosdemUrls
|
||||||
import be.digitalia.fosdem.utils.configureToolbarColors
|
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.launchAndRepeatOnLifecycle
|
||||||
import be.digitalia.fosdem.utils.toSlug
|
import be.digitalia.fosdem.utils.toSlug
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -77,14 +78,15 @@ class RoomImageDialogActivity : AppCompatActivity(R.layout.dialog_room_image) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
owner.launchAndRepeatOnLifecycle {
|
||||||
// Display the room status as subtitle
|
// Display the room status as subtitle
|
||||||
api.roomStatuses.observe(owner) { roomStatuses ->
|
api.roomStatuses.collect { statuses ->
|
||||||
val roomStatus = roomStatuses[roomName]
|
toolbar.subtitle = statuses[roomName]?.let { roomStatus ->
|
||||||
toolbar.subtitle = if (roomStatus != null) {
|
|
||||||
SpannableString(context.getString(roomStatus.nameResId)).apply {
|
SpannableString(context.getString(roomStatus.nameResId)).apply {
|
||||||
this[0, length] = ForegroundColorSpan(ContextCompat.getColor(context, roomStatus.colorResId))
|
this[0, length] = ForegroundColorSpan(ContextCompat.getColor(context, roomStatus.colorResId))
|
||||||
}
|
}
|
||||||
} else null
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,16 +11,17 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.FragmentTransaction
|
import androidx.fragment.app.FragmentTransaction
|
||||||
import androidx.fragment.app.add
|
import androidx.fragment.app.add
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
|
import androidx.fragment.app.commitNow
|
||||||
import androidx.fragment.app.replace
|
import androidx.fragment.app.replace
|
||||||
import be.digitalia.fosdem.R
|
import be.digitalia.fosdem.R
|
||||||
import be.digitalia.fosdem.fragments.EventDetailsFragment
|
import be.digitalia.fosdem.fragments.EventDetailsFragment
|
||||||
import be.digitalia.fosdem.fragments.RoomImageDialogFragment
|
import be.digitalia.fosdem.fragments.RoomImageDialogFragment
|
||||||
import be.digitalia.fosdem.fragments.TrackScheduleListFragment
|
import be.digitalia.fosdem.fragments.TrackScheduleListFragment
|
||||||
import be.digitalia.fosdem.model.Day
|
import be.digitalia.fosdem.model.Day
|
||||||
import be.digitalia.fosdem.model.Event
|
|
||||||
import be.digitalia.fosdem.model.Track
|
import be.digitalia.fosdem.model.Track
|
||||||
import be.digitalia.fosdem.utils.CreateNfcAppDataCallback
|
import be.digitalia.fosdem.utils.CreateNfcAppDataCallback
|
||||||
import be.digitalia.fosdem.utils.isLightTheme
|
import be.digitalia.fosdem.utils.isLightTheme
|
||||||
|
import be.digitalia.fosdem.utils.launchAndRepeatOnLifecycle
|
||||||
import be.digitalia.fosdem.utils.setNfcAppDataPushMessageCallbackIfAvailable
|
import be.digitalia.fosdem.utils.setNfcAppDataPushMessageCallbackIfAvailable
|
||||||
import be.digitalia.fosdem.utils.setTaskColorPrimary
|
import be.digitalia.fosdem.utils.setTaskColorPrimary
|
||||||
import be.digitalia.fosdem.utils.statusBarColorCompat
|
import be.digitalia.fosdem.utils.statusBarColorCompat
|
||||||
|
@ -99,13 +100,13 @@ class TrackScheduleActivity : AppCompatActivity(R.layout.track_schedule), Create
|
||||||
|
|
||||||
if (isTabletLandscape) {
|
if (isTabletLandscape) {
|
||||||
// Tablet mode: Show event details in the right pane fragment
|
// Tablet mode: Show event details in the right pane fragment
|
||||||
viewModel.selectedEvent.observe(this) { event: Event? ->
|
launchAndRepeatOnLifecycle {
|
||||||
|
viewModel.selectedEventFlow.collect { event ->
|
||||||
val currentFragment = fm.findFragmentById(R.id.event) as EventDetailsFragment?
|
val currentFragment = fm.findFragmentById(R.id.event) as EventDetailsFragment?
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
// Only replace the fragment if the event is different
|
// Only replace the fragment if the event is different
|
||||||
if (currentFragment?.event != event) {
|
if (currentFragment?.event != event) {
|
||||||
// Allow state loss since the event fragment will be synchronized with the list selection after activity re-creation
|
fm.commitNow {
|
||||||
fm.commit {
|
|
||||||
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||||
replace<EventDetailsFragment>(R.id.event,
|
replace<EventDetailsFragment>(R.id.event,
|
||||||
args = EventDetailsFragment.createArguments(event))
|
args = EventDetailsFragment.createArguments(event))
|
||||||
|
@ -114,12 +115,13 @@ class TrackScheduleActivity : AppCompatActivity(R.layout.track_schedule), Create
|
||||||
} else {
|
} else {
|
||||||
// Nothing is selected because the list is empty
|
// Nothing is selected because the list is empty
|
||||||
if (currentFragment != null) {
|
if (currentFragment != null) {
|
||||||
fm.commit { remove(currentFragment) }
|
fm.commitNow { remove(currentFragment) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bookmarkStatusViewModel.event = event
|
bookmarkStatusViewModel.event = event
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
findViewById<ImageButton?>(R.id.fab)?.setupBookmarkStatus(bookmarkStatusViewModel, this)
|
findViewById<ImageButton?>(R.id.fab)?.setupBookmarkStatus(bookmarkStatusViewModel, this)
|
||||||
|
|
||||||
|
@ -138,7 +140,7 @@ class TrackScheduleActivity : AppCompatActivity(R.layout.track_schedule), Create
|
||||||
// CreateNfcAppDataCallback
|
// CreateNfcAppDataCallback
|
||||||
|
|
||||||
override fun createNfcAppData(): NdefRecord? {
|
override fun createNfcAppData(): NdefRecord? {
|
||||||
return viewModel.selectedEvent.value?.toNfcAppData(this)
|
return viewModel.selectedEvent?.toNfcAppData(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
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
|
||||||
|
@ -20,6 +21,7 @@ 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 be.digitalia.fosdem.utils.CreateNfcAppDataCallback
|
import be.digitalia.fosdem.utils.CreateNfcAppDataCallback
|
||||||
|
import be.digitalia.fosdem.utils.assistedViewModels
|
||||||
import be.digitalia.fosdem.utils.enforceSingleScrollDirection
|
import be.digitalia.fosdem.utils.enforceSingleScrollDirection
|
||||||
import be.digitalia.fosdem.utils.instantiate
|
import be.digitalia.fosdem.utils.instantiate
|
||||||
import be.digitalia.fosdem.utils.isLightTheme
|
import be.digitalia.fosdem.utils.isLightTheme
|
||||||
|
@ -34,6 +36,7 @@ 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
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
|
@ -44,16 +47,23 @@ import dagger.hilt.android.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()
|
||||||
private val viewModel: TrackScheduleEventViewModel by viewModels()
|
@Inject
|
||||||
|
lateinit var viewModelFactory: TrackScheduleEventViewModel.Factory
|
||||||
|
private val viewModel: TrackScheduleEventViewModel by assistedViewModels {
|
||||||
|
viewModelFactory.create(day, track)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val day: Day by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
intent.getParcelableExtra(EXTRA_DAY)!!
|
||||||
|
}
|
||||||
|
private val track: Track by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
intent.getParcelableExtra(EXTRA_TRACK)!!
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setSupportActionBar(findViewById(R.id.bottom_appbar))
|
setSupportActionBar(findViewById(R.id.bottom_appbar))
|
||||||
|
|
||||||
val intent = intent
|
|
||||||
val day: Day = intent.getParcelableExtra(EXTRA_DAY)!!
|
|
||||||
val track: Track = intent.getParcelableExtra(EXTRA_TRACK)!!
|
|
||||||
|
|
||||||
val progress = ContentLoadingViewMediator(findViewById(R.id.progress))
|
val progress = ContentLoadingViewMediator(findViewById(R.id.progress))
|
||||||
val pager: ViewPager2 = findViewById(R.id.pager)
|
val pager: ViewPager2 = findViewById(R.id.pager)
|
||||||
pager.recyclerView.enforceSingleScrollDirection()
|
pager.recyclerView.enforceSingleScrollDirection()
|
||||||
|
@ -92,16 +102,14 @@ class TrackScheduleEventActivity : AppCompatActivity(R.layout.track_schedule_eve
|
||||||
|
|
||||||
progress.isVisible = true
|
progress.isVisible = true
|
||||||
|
|
||||||
with(viewModel) {
|
lifecycleScope.launchWhenStarted {
|
||||||
setDayAndTrack(day, track)
|
val events = viewModel.scheduleSnapshot.await()
|
||||||
scheduleSnapshot.observe(this@TrackScheduleEventActivity) { events ->
|
|
||||||
progress.isVisible = false
|
progress.isVisible = false
|
||||||
|
|
||||||
pager.isVisible = true
|
pager.isVisible = true
|
||||||
adapter.events = events
|
adapter.events = events
|
||||||
|
|
||||||
// Delay setting the adapter
|
// Delay setting the adapter to ensure the current position is restored properly
|
||||||
// to ensure the current position is restored properly
|
|
||||||
if (pager.adapter == null) {
|
if (pager.adapter == null) {
|
||||||
pager.adapter = adapter
|
pager.adapter = adapter
|
||||||
|
|
||||||
|
@ -115,7 +123,6 @@ class TrackScheduleEventActivity : AppCompatActivity(R.layout.track_schedule_eve
|
||||||
bookmarkStatusViewModel.event = adapter.events.getOrNull(pager.currentItem)
|
bookmarkStatusViewModel.event = adapter.events.getOrNull(pager.currentItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Enable Android Beam
|
// Enable Android Beam
|
||||||
setNfcAppDataPushMessageCallbackIfAvailable(this)
|
setNfcAppDataPushMessageCallbackIfAvailable(this)
|
||||||
|
|
|
@ -35,11 +35,13 @@ class BookmarksAdapter(context: Context, private val multiChoiceHelper: MultiCho
|
||||||
private val errorColor: Int
|
private val errorColor: Int
|
||||||
private val observers = SimpleArrayMap<AdapterDataObserver, BookmarksDataObserverWrapper>()
|
private val observers = SimpleArrayMap<AdapterDataObserver, BookmarksDataObserverWrapper>()
|
||||||
|
|
||||||
var roomStatuses: Map<String, RoomStatus>? = null
|
var roomStatuses: Map<String, RoomStatus> = emptyMap()
|
||||||
set(value) {
|
set(value) {
|
||||||
|
if (field != value) {
|
||||||
field = value
|
field = value
|
||||||
notifyItemRangeChanged(0, itemCount, DETAILS_PAYLOAD)
|
notifyItemRangeChanged(0, itemCount, DETAILS_PAYLOAD)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setHasStableIds(true)
|
setHasStableIds(true)
|
||||||
|
@ -56,16 +58,12 @@ class BookmarksAdapter(context: Context, private val multiChoiceHelper: MultiCho
|
||||||
return ViewHolder(view, multiChoiceHelper, timeFormatter, errorColor)
|
return ViewHolder(view, multiChoiceHelper, timeFormatter, errorColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getRoomStatus(event: Event): RoomStatus? {
|
|
||||||
return roomStatuses?.get(event.roomName)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val event = getItem(position)
|
val event = getItem(position)
|
||||||
holder.bind(event)
|
holder.bind(event)
|
||||||
val previous = if (position > 0) getItem(position - 1) else null
|
val previous = if (position > 0) getItem(position - 1) else null
|
||||||
val next = if (position + 1 < itemCount) getItem(position + 1) else null
|
val next = if (position + 1 < itemCount) getItem(position + 1) else null
|
||||||
holder.bindDetails(event, previous, next, getRoomStatus(event))
|
holder.bindDetails(event, previous, next, roomStatuses[event.roomName])
|
||||||
holder.bindSelection()
|
holder.bindSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +75,7 @@ class BookmarksAdapter(context: Context, private val multiChoiceHelper: MultiCho
|
||||||
if (DETAILS_PAYLOAD in payloads) {
|
if (DETAILS_PAYLOAD in payloads) {
|
||||||
val previous = if (position > 0) getItem(position - 1) else null
|
val previous = if (position > 0) getItem(position - 1) else null
|
||||||
val next = if (position + 1 < itemCount) getItem(position + 1) else null
|
val next = if (position + 1 < itemCount) getItem(position + 1) else null
|
||||||
holder.bindDetails(event, previous, next, getRoomStatus(event))
|
holder.bindDetails(event, previous, next, roomStatuses[event.roomName])
|
||||||
}
|
}
|
||||||
if (MultiChoiceHelper.SELECTION_PAYLOAD in payloads) {
|
if (MultiChoiceHelper.SELECTION_PAYLOAD in payloads) {
|
||||||
holder.bindSelection()
|
holder.bindSelection()
|
||||||
|
|
|
@ -12,7 +12,7 @@ import androidx.appcompat.content.res.AppCompatResources
|
||||||
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.paging.PagedListAdapter
|
import androidx.paging.PagingDataAdapter
|
||||||
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
|
||||||
|
@ -23,15 +23,17 @@ import be.digitalia.fosdem.utils.DateUtils
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
class EventsAdapter constructor(context: Context, private val showDay: Boolean = true) :
|
class EventsAdapter constructor(context: Context, private val showDay: Boolean = true) :
|
||||||
PagedListAdapter<StatusEvent, EventsAdapter.ViewHolder>(DIFF_CALLBACK) {
|
PagingDataAdapter<StatusEvent, EventsAdapter.ViewHolder>(DIFF_CALLBACK) {
|
||||||
|
|
||||||
private val timeFormatter = DateUtils.getTimeFormatter(context)
|
private val timeFormatter = DateUtils.getTimeFormatter(context)
|
||||||
|
|
||||||
var roomStatuses: Map<String, RoomStatus>? = null
|
var roomStatuses: Map<String, RoomStatus> = emptyMap()
|
||||||
set(value) {
|
set(value) {
|
||||||
|
if (field != value) {
|
||||||
field = value
|
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
|
||||||
|
|
||||||
|
@ -40,10 +42,6 @@ class EventsAdapter constructor(context: Context, private val showDay: Boolean =
|
||||||
return ViewHolder(view, timeFormatter)
|
return ViewHolder(view, timeFormatter)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getRoomStatus(event: Event): RoomStatus? {
|
|
||||||
return roomStatuses?.get(event.roomName)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val statusEvent = getItem(position)
|
val statusEvent = getItem(position)
|
||||||
if (statusEvent == null) {
|
if (statusEvent == null) {
|
||||||
|
@ -51,7 +49,7 @@ class EventsAdapter constructor(context: Context, private val showDay: Boolean =
|
||||||
} else {
|
} else {
|
||||||
val event = statusEvent.event
|
val event = statusEvent.event
|
||||||
holder.bind(event, statusEvent.isBookmarked)
|
holder.bind(event, statusEvent.isBookmarked)
|
||||||
holder.bindDetails(event, showDay, getRoomStatus(event))
|
holder.bindDetails(event, showDay, roomStatuses[event.roomName])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +61,7 @@ class EventsAdapter constructor(context: Context, private val showDay: Boolean =
|
||||||
if (statusEvent != null) {
|
if (statusEvent != null) {
|
||||||
if (DETAILS_PAYLOAD in payloads) {
|
if (DETAILS_PAYLOAD in payloads) {
|
||||||
val event = statusEvent.event
|
val event = statusEvent.event
|
||||||
holder.bindDetails(event, showDay, getRoomStatus(event))
|
holder.bindDetails(event, showDay, roomStatuses[event.roomName])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,11 @@ package be.digitalia.fosdem.api
|
||||||
|
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.asLiveData
|
|
||||||
import androidx.lifecycle.distinctUntilChanged
|
|
||||||
import androidx.lifecycle.liveData
|
|
||||||
import androidx.lifecycle.switchMap
|
|
||||||
import be.digitalia.fosdem.alarms.AppAlarmManager
|
import be.digitalia.fosdem.alarms.AppAlarmManager
|
||||||
import be.digitalia.fosdem.db.ScheduleDao
|
import be.digitalia.fosdem.db.ScheduleDao
|
||||||
import be.digitalia.fosdem.livedata.LiveDataFactory.scheduler
|
import be.digitalia.fosdem.flow.flowWhileShared
|
||||||
import be.digitalia.fosdem.livedata.SingleEvent
|
import be.digitalia.fosdem.flow.schedulerFlow
|
||||||
|
import be.digitalia.fosdem.flow.stateFlow
|
||||||
import be.digitalia.fosdem.model.DownloadScheduleResult
|
import be.digitalia.fosdem.model.DownloadScheduleResult
|
||||||
import be.digitalia.fosdem.model.LoadingState
|
import be.digitalia.fosdem.model.LoadingState
|
||||||
import be.digitalia.fosdem.model.RoomStatus
|
import be.digitalia.fosdem.model.RoomStatus
|
||||||
|
@ -22,9 +17,20 @@ import be.digitalia.fosdem.utils.ByteCountSource
|
||||||
import be.digitalia.fosdem.utils.DateUtils
|
import be.digitalia.fosdem.utils.DateUtils
|
||||||
import be.digitalia.fosdem.utils.network.HttpClient
|
import be.digitalia.fosdem.utils.network.HttpClient
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
|
@ -45,12 +51,13 @@ class FosdemApi @Inject constructor(
|
||||||
private val alarmManager: AppAlarmManager
|
private val alarmManager: AppAlarmManager
|
||||||
) {
|
) {
|
||||||
private var downloadJob: Job? = null
|
private var downloadJob: Job? = null
|
||||||
private val _downloadScheduleState = MutableLiveData<LoadingState<DownloadScheduleResult>>()
|
private val _downloadScheduleState =
|
||||||
|
MutableStateFlow<LoadingState<DownloadScheduleResult>>(LoadingState.Idle())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download & store the schedule to the database.
|
* Download & store the schedule to the database.
|
||||||
* Only a single Job will be active at a time.
|
* Only a single Job will be active at a time.
|
||||||
* The result will be sent back through downloadScheduleResult LiveData.
|
* The result will be notified through downloadScheduleState StateFlow.
|
||||||
*/
|
*/
|
||||||
@MainThread
|
@MainThread
|
||||||
fun downloadSchedule(): Job {
|
fun downloadSchedule(): Job {
|
||||||
|
@ -73,7 +80,7 @@ class FosdemApi @Inject constructor(
|
||||||
ByteCountSource(body.source(), length / 10L) { byteCount ->
|
ByteCountSource(body.source(), length / 10L) { byteCount ->
|
||||||
// Cap percent to 100
|
// Cap percent to 100
|
||||||
val percent = (byteCount * 100L / length).toInt().coerceAtMost(100)
|
val percent = (byteCount * 100L / length).toInt().coerceAtMost(100)
|
||||||
_downloadScheduleState.postValue(LoadingState.Loading(percent))
|
_downloadScheduleState.value = LoadingState.Loading(percent)
|
||||||
}.buffer()
|
}.buffer()
|
||||||
} else {
|
} else {
|
||||||
body.source()
|
body.source()
|
||||||
|
@ -92,16 +99,24 @@ class FosdemApi @Inject constructor(
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
DownloadScheduleResult.Error
|
DownloadScheduleResult.Error
|
||||||
}
|
}
|
||||||
_downloadScheduleState.value = LoadingState.Idle(SingleEvent(res))
|
_downloadScheduleState.value = LoadingState.Idle(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadScheduleState: LiveData<LoadingState<DownloadScheduleResult>>
|
val downloadScheduleState: StateFlow<LoadingState<DownloadScheduleResult>> =
|
||||||
get() = _downloadScheduleState
|
_downloadScheduleState.asStateFlow()
|
||||||
|
|
||||||
val roomStatuses: LiveData<Map<String, RoomStatus>> by lazy(LazyThreadSafetyMode.NONE) {
|
fun downloadScheduleResultConsumed() {
|
||||||
|
_downloadScheduleState.update { state ->
|
||||||
|
if (state is LoadingState.Idle) LoadingState.Idle() else state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val roomStatuses: Flow<Map<String, RoomStatus>> by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
stateFlow(BackgroundWorkScope, emptyMap()) { subscriptionCount ->
|
||||||
// The room statuses will only be loaded when the event is live.
|
// The room statuses will only be loaded when the event is live.
|
||||||
// Use the days from the database to determine it.
|
// Use the days from the database to determine it.
|
||||||
val scheduler = scheduleDao.days.asLiveData().distinctUntilChanged().switchMap { days ->
|
val scheduler = scheduleDao.days.flatMapLatest { days ->
|
||||||
val startEndTimestamps = LongArray(days.size * 2)
|
val startEndTimestamps = LongArray(days.size * 2)
|
||||||
var index = 0
|
var index = 0
|
||||||
for (day in days) {
|
for (day in days) {
|
||||||
|
@ -112,36 +127,40 @@ class FosdemApi @Inject constructor(
|
||||||
.atZone(DateUtils.conferenceZoneId)
|
.atZone(DateUtils.conferenceZoneId)
|
||||||
.toEpochSecond() * 1000L
|
.toEpochSecond() * 1000L
|
||||||
}
|
}
|
||||||
scheduler(*startEndTimestamps)
|
schedulerFlow(*startEndTimestamps)
|
||||||
|
.flowWhileShared(subscriptionCount, SharingStarted.WhileSubscribed())
|
||||||
|
}
|
||||||
|
scheduler.distinctUntilChanged().flatMapLatest { isLive ->
|
||||||
|
if (isLive) {
|
||||||
|
buildLiveRoomStatusesFlow()
|
||||||
|
.flowWhileShared(subscriptionCount, SharingStarted.WhileSubscribed(5000L))
|
||||||
|
}
|
||||||
|
else flowOf(emptyMap())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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
|
// Implementors: replace the above code block with the next line to disable room status support
|
||||||
// MutableLiveData()
|
// emptyFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a LiveData instance which loads and refreshes the Room statuses during the event.
|
* Builds a stateful cold Flow which loads and refreshes the Room statuses during the event.
|
||||||
*/
|
*/
|
||||||
private fun buildLiveRoomStatusesLiveData(): LiveData<Map<String, RoomStatus>> {
|
private fun buildLiveRoomStatusesFlow(): Flow<Map<String, RoomStatus>> {
|
||||||
var nextRefreshTime = 0L
|
var nextRefreshTime = 0L
|
||||||
var expirationTime = Long.MAX_VALUE
|
var expirationTime = Long.MAX_VALUE
|
||||||
var retryAttempt = 0
|
var retryAttempt = 0
|
||||||
|
|
||||||
return liveData {
|
return flow {
|
||||||
var now = SystemClock.elapsedRealtime()
|
var now = SystemClock.elapsedRealtime()
|
||||||
var nextRefreshDelay = nextRefreshTime - now
|
var nextRefreshDelay = nextRefreshTime - now
|
||||||
|
|
||||||
if (now > expirationTime && latestValue?.isEmpty() == false) {
|
if (now > expirationTime) {
|
||||||
// When the data expires, replace it with an empty value
|
// When the data expires, replace it with an empty value
|
||||||
emit(emptyMap())
|
emit(emptyMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (nextRefreshDelay > 0) {
|
|
||||||
delay(nextRefreshDelay)
|
delay(nextRefreshDelay)
|
||||||
}
|
|
||||||
|
|
||||||
nextRefreshDelay = try {
|
nextRefreshDelay = try {
|
||||||
val response = httpClient.get(FosdemUrls.rooms) { body, _ ->
|
val response = httpClient.get(FosdemUrls.rooms) { body, _ ->
|
||||||
|
@ -159,7 +178,7 @@ class FosdemApi @Inject constructor(
|
||||||
}
|
}
|
||||||
now = SystemClock.elapsedRealtime()
|
now = SystemClock.elapsedRealtime()
|
||||||
|
|
||||||
if (now > expirationTime && latestValue?.isEmpty() == false) {
|
if (now > expirationTime) {
|
||||||
emit(emptyMap())
|
emit(emptyMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package be.digitalia.fosdem.db
|
package be.digitalia.fosdem.db
|
||||||
|
|
||||||
import androidx.annotation.WorkerThread
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
|
@ -10,16 +8,21 @@ import androidx.room.Transaction
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import be.digitalia.fosdem.db.converters.NonNullInstantTypeConverters
|
import be.digitalia.fosdem.db.converters.NonNullInstantTypeConverters
|
||||||
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.model.AlarmInfo
|
import be.digitalia.fosdem.model.AlarmInfo
|
||||||
import be.digitalia.fosdem.model.Event
|
import be.digitalia.fosdem.model.Event
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class BookmarksDao(private val appDatabase: AppDatabase) {
|
abstract class BookmarksDao(appDatabase: AppDatabase) {
|
||||||
|
val version: StateFlow<Int> =
|
||||||
|
appDatabase.createVersionFlow(EventEntity.TABLE_NAME, Bookmark.TABLE_NAME)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the bookmarks.
|
* Returns the bookmarks.
|
||||||
*
|
*
|
||||||
* @param minStartTime When greater than Instant.EPOCH, only return the events starting after this time.
|
* @param minStartTime Only return the events starting after this time.
|
||||||
*/
|
*/
|
||||||
@Query("""SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description,
|
@Query("""SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description,
|
||||||
GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type
|
GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type
|
||||||
|
@ -34,21 +37,7 @@ abstract class BookmarksDao(private val appDatabase: AppDatabase) {
|
||||||
GROUP BY e.id
|
GROUP BY e.id
|
||||||
ORDER BY e.start_time ASC""")
|
ORDER BY e.start_time ASC""")
|
||||||
@TypeConverters(NonNullInstantTypeConverters::class)
|
@TypeConverters(NonNullInstantTypeConverters::class)
|
||||||
abstract fun getBookmarks(minStartTime: Instant): LiveData<List<Event>>
|
abstract suspend fun getBookmarks(minStartTime: Instant = Instant.EPOCH): List<Event>
|
||||||
|
|
||||||
@Query("""SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description,
|
|
||||||
GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type
|
|
||||||
FROM bookmarks b
|
|
||||||
JOIN events e ON b.event_id = e.id
|
|
||||||
JOIN events_titles et ON e.id = et.`rowid`
|
|
||||||
JOIN days d ON e.day_index = d.`index`
|
|
||||||
JOIN tracks t ON e.track_id = t.id
|
|
||||||
LEFT JOIN events_persons ep ON e.id = ep.event_id
|
|
||||||
LEFT JOIN persons p ON ep.person_id = p.`rowid`
|
|
||||||
GROUP BY e.id
|
|
||||||
ORDER BY e.start_time ASC""")
|
|
||||||
@WorkerThread
|
|
||||||
abstract fun getBookmarks(): List<Event>
|
|
||||||
|
|
||||||
@Query("""SELECT b.event_id, e.start_time
|
@Query("""SELECT b.event_id, e.start_time
|
||||||
FROM bookmarks b
|
FROM bookmarks b
|
||||||
|
@ -59,7 +48,7 @@ abstract class BookmarksDao(private val appDatabase: AppDatabase) {
|
||||||
abstract suspend fun getBookmarksAlarmInfo(minStartTime: Instant): List<AlarmInfo>
|
abstract suspend fun getBookmarksAlarmInfo(minStartTime: Instant): List<AlarmInfo>
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM bookmarks WHERE event_id = :event")
|
@Query("SELECT COUNT(*) FROM bookmarks WHERE event_id = :event")
|
||||||
abstract fun getBookmarkStatus(event: Event): LiveData<Boolean>
|
abstract suspend fun getBookmarkStatus(event: Event): Boolean
|
||||||
|
|
||||||
suspend fun addBookmark(event: Event): AlarmInfo? {
|
suspend fun addBookmark(event: Event): AlarmInfo? {
|
||||||
val ids = addBookmarksInternal(listOf(Bookmark(event.id)))
|
val ids = addBookmarksInternal(listOf(Bookmark(event.id)))
|
||||||
|
|
18
app/src/main/java/be/digitalia/fosdem/db/RoomDatabaseExt.kt
Normal file
18
app/src/main/java/be/digitalia/fosdem/db/RoomDatabaseExt.kt
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package be.digitalia.fosdem.db
|
||||||
|
|
||||||
|
import androidx.room.InvalidationTracker
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
|
fun RoomDatabase.createVersionFlow(vararg tables: String): StateFlow<Int> {
|
||||||
|
val stateFlow = MutableStateFlow(0)
|
||||||
|
invalidationTracker.addObserver(object : InvalidationTracker.Observer(tables) {
|
||||||
|
override fun onInvalidated(tables: MutableSet<String>) {
|
||||||
|
stateFlow.update { it + 1 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return stateFlow.asStateFlow()
|
||||||
|
}
|
|
@ -4,9 +4,7 @@ import androidx.annotation.WorkerThread
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.longPreferencesKey
|
import androidx.datastore.preferences.core.longPreferencesKey
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.paging.PagingSource
|
||||||
import androidx.lifecycle.liveData
|
|
||||||
import androidx.paging.DataSource
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
|
@ -26,20 +24,27 @@ import be.digitalia.fosdem.model.Person
|
||||||
import be.digitalia.fosdem.model.StatusEvent
|
import be.digitalia.fosdem.model.StatusEvent
|
||||||
import be.digitalia.fosdem.model.Track
|
import be.digitalia.fosdem.model.Track
|
||||||
import be.digitalia.fosdem.utils.BackgroundWorkScope
|
import be.digitalia.fosdem.utils.BackgroundWorkScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.shareIn
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.util.HashSet
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
||||||
|
val version: StateFlow<Int> =
|
||||||
|
appDatabase.createVersionFlow(EventEntity.TABLE_NAME)
|
||||||
|
val bookmarksVersion: StateFlow<Int>
|
||||||
|
get() = appDatabase.bookmarksDao.version
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The latest update time, or null if not available.
|
* @return The latest update time, or null if not available.
|
||||||
|
@ -218,16 +223,17 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
||||||
protected abstract fun clearDays()
|
protected abstract fun clearDays()
|
||||||
|
|
||||||
// Cache days
|
// Cache days
|
||||||
val days: Flow<List<Day>> by lazy {
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
getDaysInternal().shareIn(
|
val days: Flow<List<Day>> = appDatabase.createVersionFlow(Day.TABLE_NAME)
|
||||||
|
.mapLatest { getDaysInternal() }
|
||||||
|
.stateIn(
|
||||||
scope = BackgroundWorkScope,
|
scope = BackgroundWorkScope,
|
||||||
started = SharingStarted.Eagerly,
|
started = SharingStarted.Lazily,
|
||||||
replay = 1
|
initialValue = null
|
||||||
)
|
).filterNotNull()
|
||||||
}
|
|
||||||
|
|
||||||
@Query("SELECT `index`, date FROM days ORDER BY `index` ASC")
|
@Query("SELECT `index`, date FROM days ORDER BY `index` ASC")
|
||||||
protected abstract fun getDaysInternal(): Flow<List<Day>>
|
protected abstract suspend fun getDaysInternal(): List<Day>
|
||||||
|
|
||||||
suspend fun getYear(): Int {
|
suspend fun getYear(): Int {
|
||||||
// Compute from days if available, fall back to current year
|
// Compute from days if available, fall back to current year
|
||||||
|
@ -240,7 +246,7 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
||||||
WHERE e.day_index = :day
|
WHERE e.day_index = :day
|
||||||
GROUP BY t.id
|
GROUP BY t.id
|
||||||
ORDER BY t.name ASC""")
|
ORDER BY t.name ASC""")
|
||||||
abstract fun getTracks(day: Day): LiveData<List<Track>>
|
abstract suspend fun getTracks(day: Day): List<Track>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the event with the specified id, or null if not found.
|
* Returns the event with the specified id, or null if not found.
|
||||||
|
@ -273,10 +279,10 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
||||||
WHERE e.id IN (:ids)
|
WHERE e.id IN (:ids)
|
||||||
GROUP BY e.id
|
GROUP BY e.id
|
||||||
ORDER BY e.start_time ASC""")
|
ORDER BY e.start_time ASC""")
|
||||||
abstract fun getEvents(ids: LongArray): DataSource.Factory<Int, StatusEvent>
|
abstract fun getEvents(ids: LongArray): PagingSource<Int, StatusEvent>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the events for a specified track.
|
* Returns the events for a specified track, including their bookmark status.
|
||||||
*/
|
*/
|
||||||
@Query("""SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description,
|
@Query("""SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description,
|
||||||
GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type,
|
GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type,
|
||||||
|
@ -291,10 +297,10 @@ 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 getEvents(day: Day, track: Track): LiveData<List<StatusEvent>>
|
abstract suspend fun getEvents(day: Day, track: Track): List<StatusEvent>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a snapshot of the events for a specified track (without the bookmark status).
|
* Returns the events for a specified track, without their bookmark status.
|
||||||
*/
|
*/
|
||||||
@Query("""SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description,
|
@Query("""SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description,
|
||||||
GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type
|
GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type
|
||||||
|
@ -307,7 +313,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 suspend fun getEventsSnapshot(day: Day, track: Track): List<Event>
|
abstract suspend fun getEventsWithoutBookmarkStatus(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.
|
||||||
|
@ -326,7 +332,7 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
||||||
GROUP BY e.id
|
GROUP BY e.id
|
||||||
ORDER BY e.start_time ASC""")
|
ORDER BY e.start_time ASC""")
|
||||||
@TypeConverters(NonNullInstantTypeConverters::class)
|
@TypeConverters(NonNullInstantTypeConverters::class)
|
||||||
abstract fun getEventsWithStartTime(minStartTime: Instant, maxStartTime: Instant): DataSource.Factory<Int, StatusEvent>
|
abstract fun getEventsWithStartTime(minStartTime: Instant, maxStartTime: Instant): PagingSource<Int, StatusEvent>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns events in progress at the specified time, ordered by descending start time.
|
* Returns events in progress at the specified time, ordered by descending start time.
|
||||||
|
@ -345,7 +351,7 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
||||||
GROUP BY e.id
|
GROUP BY e.id
|
||||||
ORDER BY e.start_time DESC""")
|
ORDER BY e.start_time DESC""")
|
||||||
@TypeConverters(NonNullInstantTypeConverters::class)
|
@TypeConverters(NonNullInstantTypeConverters::class)
|
||||||
abstract fun getEventsInProgress(time: Instant): DataSource.Factory<Int, StatusEvent>
|
abstract fun getEventsInProgress(time: Instant): PagingSource<Int, StatusEvent>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the events presented by the specified person.
|
* Returns the events presented by the specified person.
|
||||||
|
@ -363,7 +369,7 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
||||||
WHERE ep2.person_id = :person
|
WHERE ep2.person_id = :person
|
||||||
GROUP BY e.id
|
GROUP BY e.id
|
||||||
ORDER BY e.start_time ASC""")
|
ORDER BY e.start_time ASC""")
|
||||||
abstract fun getEvents(person: Person): DataSource.Factory<Int, StatusEvent>
|
abstract fun getEvents(person: Person): PagingSource<Int, StatusEvent>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search through matching titles, subtitles, track names, person names.
|
* Search through matching titles, subtitles, track names, person names.
|
||||||
|
@ -397,7 +403,7 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
||||||
)
|
)
|
||||||
GROUP BY e.id
|
GROUP BY e.id
|
||||||
ORDER BY e.start_time ASC""")
|
ORDER BY e.start_time ASC""")
|
||||||
abstract fun getSearchResults(query: String): DataSource.Factory<Int, StatusEvent>
|
abstract fun getSearchResults(query: String): PagingSource<Int, StatusEvent>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all persons in alphabetical order.
|
* Returns all persons in alphabetical order.
|
||||||
|
@ -405,16 +411,14 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
||||||
@Query("""SELECT `rowid`, name
|
@Query("""SELECT `rowid`, name
|
||||||
FROM persons
|
FROM persons
|
||||||
ORDER BY name COLLATE NOCASE""")
|
ORDER BY name COLLATE NOCASE""")
|
||||||
abstract fun getPersons(): DataSource.Factory<Int, Person>
|
abstract fun getPersons(): PagingSource<Int, Person>
|
||||||
|
|
||||||
fun getEventDetails(event: Event): LiveData<EventDetails> {
|
suspend fun getEventDetails(event: Event): EventDetails {
|
||||||
return liveData {
|
// Load persons and links in parallel
|
||||||
// Load persons and links in parallel as soon as the LiveData becomes active
|
return coroutineScope {
|
||||||
coroutineScope {
|
|
||||||
val persons = async { getPersons(event) }
|
val persons = async { getPersons(event) }
|
||||||
val links = async { getLinks(event) }
|
val links = async { getLinks(event) }
|
||||||
emit(EventDetails(persons.await(), links.await()))
|
EventDetails(persons.await(), links.await())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
74
app/src/main/java/be/digitalia/fosdem/flow/FlowExt.kt
Normal file
74
app/src/main/java/be/digitalia/fosdem/flow/FlowExt.kt
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
package be.digitalia.fosdem.flow
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingCommand
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
fun <T> stateFlow(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
initialValue: T,
|
||||||
|
producer: (subscriptionCount: StateFlow<Int>) -> Flow<T>
|
||||||
|
): StateFlow<T> {
|
||||||
|
val state = MutableStateFlow(initialValue)
|
||||||
|
scope.launch {
|
||||||
|
producer(state.subscriptionCount).collect(state)
|
||||||
|
}
|
||||||
|
return state.asStateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun <T> Flow<T>.flowWhileShared(
|
||||||
|
subscriptionCount: StateFlow<Int>,
|
||||||
|
started: SharingStarted
|
||||||
|
): Flow<T> {
|
||||||
|
return started.command(subscriptionCount)
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.flatMapLatest {
|
||||||
|
when (it) {
|
||||||
|
SharingCommand.START -> this
|
||||||
|
SharingCommand.STOP,
|
||||||
|
SharingCommand.STOP_AND_RESET_REPLAY_CACHE -> emptyFlow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> countSubscriptionsFlow(producer: (subscriptionCount: StateFlow<Int>) -> Flow<T>): Flow<T> {
|
||||||
|
val subscriptionCount = MutableStateFlow(0)
|
||||||
|
return producer(subscriptionCount.asStateFlow())
|
||||||
|
.countSubscriptionsTo(subscriptionCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Flow<T>.countSubscriptionsTo(subscriptionCount: MutableStateFlow<Int>): Flow<T> {
|
||||||
|
return flow {
|
||||||
|
subscriptionCount.update { it + 1 }
|
||||||
|
try {
|
||||||
|
collect(this)
|
||||||
|
} finally {
|
||||||
|
subscriptionCount.update { it - 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun <T> versionedResourceFlow(
|
||||||
|
version: StateFlow<Int>,
|
||||||
|
subscriptionCount: StateFlow<Int>,
|
||||||
|
producer: suspend (version: Int) -> T
|
||||||
|
): Flow<T> {
|
||||||
|
return version
|
||||||
|
.flowWhileShared(subscriptionCount, SharingStarted.WhileSubscribed())
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.mapLatest(producer)
|
||||||
|
}
|
60
app/src/main/java/be/digitalia/fosdem/flow/Timers.kt
Normal file
60
app/src/main/java/be/digitalia/fosdem/flow/Timers.kt
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package be.digitalia.fosdem.flow
|
||||||
|
|
||||||
|
import android.os.SystemClock
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import java.util.Arrays
|
||||||
|
|
||||||
|
fun tickerFlow(periodInMillis: Long): Flow<Unit> = flow {
|
||||||
|
while (true) {
|
||||||
|
emit(Unit)
|
||||||
|
delay(periodInMillis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a ticker Flow which remembers the time of the last emission of the previous collection.
|
||||||
|
* It only supports one subscriber at a time.
|
||||||
|
*/
|
||||||
|
fun rememberTickerFlow(periodInMillis: Long): Flow<Unit> {
|
||||||
|
var nextEmissionTime = 0L
|
||||||
|
return flow {
|
||||||
|
delay(nextEmissionTime - SystemClock.elapsedRealtime())
|
||||||
|
while (true) {
|
||||||
|
emit(Unit)
|
||||||
|
nextEmissionTime = SystemClock.elapsedRealtime() + periodInMillis
|
||||||
|
delay(periodInMillis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a Flow whose value is true during scheduled periods.
|
||||||
|
*
|
||||||
|
* @param startEndTimestamps a list of timestamps in milliseconds, sorted in chronological order.
|
||||||
|
* Odd and even values represent beginnings and ends of periods, respectively.
|
||||||
|
*/
|
||||||
|
fun schedulerFlow(vararg startEndTimestamps: Long): Flow<Boolean> {
|
||||||
|
return flow {
|
||||||
|
var now = System.currentTimeMillis()
|
||||||
|
var pos = Arrays.binarySearch(startEndTimestamps, now)
|
||||||
|
while (true) {
|
||||||
|
val size = startEndTimestamps.size
|
||||||
|
if (pos >= 0) {
|
||||||
|
do {
|
||||||
|
pos++
|
||||||
|
} while (pos < size && startEndTimestamps[pos] == now)
|
||||||
|
} else {
|
||||||
|
pos = pos.inv()
|
||||||
|
}
|
||||||
|
emit(pos % 2 != 0)
|
||||||
|
if (pos == size) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Readjust current time after suspending emit()
|
||||||
|
delay(startEndTimestamps[pos] - System.currentTimeMillis())
|
||||||
|
now = startEndTimestamps[pos]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,11 +26,14 @@ import be.digitalia.fosdem.adapters.BookmarksAdapter
|
||||||
import be.digitalia.fosdem.api.FosdemApi
|
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.launchAndRepeatOnLifecycle
|
||||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.util.concurrent.CancellationException
|
import java.util.concurrent.CancellationException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
|
@ -116,15 +119,21 @@ class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataC
|
||||||
isProgressBarVisible = true
|
isProgressBarVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
api.roomStatuses.observe(viewLifecycleOwner) { statuses ->
|
viewLifecycleOwner.launchAndRepeatOnLifecycle {
|
||||||
|
launch {
|
||||||
|
api.roomStatuses.collect { statuses ->
|
||||||
adapter.roomStatuses = statuses
|
adapter.roomStatuses = statuses
|
||||||
}
|
}
|
||||||
viewModel.bookmarks.observe(viewLifecycleOwner) { bookmarks ->
|
}
|
||||||
|
launch {
|
||||||
|
viewModel.bookmarks.filterNotNull().collect { bookmarks ->
|
||||||
adapter.submitList(bookmarks)
|
adapter.submitList(bookmarks)
|
||||||
multiChoiceHelper.setAdapter(adapter, viewLifecycleOwner)
|
multiChoiceHelper.setAdapter(adapter, viewLifecycleOwner)
|
||||||
holder.isProgressBarVisible = false
|
holder.isProgressBarVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.bookmarks, menu)
|
inflater.inflate(R.menu.bookmarks, menu)
|
||||||
|
@ -196,9 +205,7 @@ class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataC
|
||||||
override fun createNfcAppData(): NdefRecord? {
|
override fun createNfcAppData(): NdefRecord? {
|
||||||
val context = context ?: return null
|
val context = context ?: return null
|
||||||
val bookmarks = viewModel.bookmarks.value
|
val bookmarks = viewModel.bookmarks.value
|
||||||
return if (bookmarks.isNullOrEmpty()) {
|
return if (bookmarks.isNullOrEmpty()) null else bookmarks.toBookmarksNfcAppData(context)
|
||||||
null
|
|
||||||
} else bookmarks.toBookmarksNfcAppData(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -27,7 +27,7 @@ import androidx.core.view.plusAssign
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.add
|
import androidx.fragment.app.add
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.lifecycle.lifecycleScope
|
||||||
import be.digitalia.fosdem.R
|
import be.digitalia.fosdem.R
|
||||||
import be.digitalia.fosdem.activities.PersonInfoActivity
|
import be.digitalia.fosdem.activities.PersonInfoActivity
|
||||||
import be.digitalia.fosdem.api.FosdemApi
|
import be.digitalia.fosdem.api.FosdemApi
|
||||||
|
@ -38,7 +38,9 @@ import be.digitalia.fosdem.model.Link
|
||||||
import be.digitalia.fosdem.model.Person
|
import be.digitalia.fosdem.model.Person
|
||||||
import be.digitalia.fosdem.utils.ClickableArrowKeyMovementMethod
|
import be.digitalia.fosdem.utils.ClickableArrowKeyMovementMethod
|
||||||
import be.digitalia.fosdem.utils.DateUtils
|
import be.digitalia.fosdem.utils.DateUtils
|
||||||
|
import be.digitalia.fosdem.utils.assistedViewModels
|
||||||
import be.digitalia.fosdem.utils.configureToolbarColors
|
import be.digitalia.fosdem.utils.configureToolbarColors
|
||||||
|
import be.digitalia.fosdem.utils.launchAndRepeatOnLifecycle
|
||||||
import be.digitalia.fosdem.utils.parseHtml
|
import be.digitalia.fosdem.utils.parseHtml
|
||||||
import be.digitalia.fosdem.utils.roomNameToResourceName
|
import be.digitalia.fosdem.utils.roomNameToResourceName
|
||||||
import be.digitalia.fosdem.utils.stripHtml
|
import be.digitalia.fosdem.utils.stripHtml
|
||||||
|
@ -59,7 +61,11 @@ class EventDetailsFragment : Fragment(R.layout.fragment_event_details) {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var api: FosdemApi
|
lateinit var api: FosdemApi
|
||||||
private val viewModel: EventDetailsViewModel by viewModels()
|
@Inject
|
||||||
|
lateinit var viewModelFactory: EventDetailsViewModel.Factory
|
||||||
|
private val viewModel: EventDetailsViewModel by assistedViewModels {
|
||||||
|
viewModelFactory.create(event)
|
||||||
|
}
|
||||||
|
|
||||||
val event by lazy<Event>(LazyThreadSafetyMode.NONE) {
|
val event by lazy<Event>(LazyThreadSafetyMode.NONE) {
|
||||||
requireArguments().getParcelable(ARG_EVENT)!!
|
requireArguments().getParcelable(ARG_EVENT)!!
|
||||||
|
@ -157,24 +163,25 @@ class EventDetailsFragment : Fragment(R.layout.fragment_event_details) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(viewModel) {
|
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
|
||||||
setEvent(event)
|
showEventDetails(holder, viewModel.eventDetails.await())
|
||||||
eventDetails.observe(viewLifecycleOwner) { eventDetails ->
|
|
||||||
showEventDetails(holder, eventDetails)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Live room status
|
// Live room status
|
||||||
val roomName = event.roomName
|
val roomName = event.roomName
|
||||||
if (!roomName.isNullOrEmpty()) {
|
if (!roomName.isNullOrEmpty()) {
|
||||||
|
viewLifecycleOwner.launchAndRepeatOnLifecycle {
|
||||||
|
api.roomStatuses.collect { statuses ->
|
||||||
holder.roomStatusTextView.run {
|
holder.roomStatusTextView.run {
|
||||||
api.roomStatuses.observe(viewLifecycleOwner) { roomStatuses ->
|
val roomStatus = statuses[roomName]
|
||||||
val roomStatus = roomStatuses[roomName]
|
|
||||||
if (roomStatus == null) {
|
if (roomStatus == null) {
|
||||||
text = null
|
text = null
|
||||||
} else {
|
} else {
|
||||||
setText(roomStatus.nameResId)
|
setText(roomStatus.nameResId)
|
||||||
setTextColor(ContextCompat.getColorStateList(context, roomStatus.colorResId))
|
setTextColor(
|
||||||
|
ContextCompat.getColorStateList(context, roomStatus.colorResId)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,9 +226,9 @@ class EventDetailsFragment : Fragment(R.layout.fragment_event_details) {
|
||||||
}
|
}
|
||||||
description = description.stripHtml()
|
description = description.stripHtml()
|
||||||
// Add speaker info if available
|
// Add speaker info if available
|
||||||
val personsCount = viewModel.eventDetails.value?.persons?.size ?: 0
|
val personsSummary = event.personsSummary
|
||||||
if (personsCount > 0) {
|
if (!personsSummary.isNullOrBlank()) {
|
||||||
val personsSummary = event.personsSummary ?: "?"
|
val personsCount = personsSummary.count { it == ',' } + 1
|
||||||
val speakersLabel = resources.getQuantityString(R.plurals.speakers, personsCount)
|
val speakersLabel = resources.getQuantityString(R.plurals.speakers, personsCount)
|
||||||
description = "$speakersLabel: $personsSummary\n\n$description"
|
description = "$speakersLabel: $personsSummary\n\n$description"
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,19 +6,26 @@ import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.setFragmentResult
|
import androidx.fragment.app.setFragmentResult
|
||||||
import androidx.fragment.app.setFragmentResultListener
|
import androidx.fragment.app.setFragmentResultListener
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.paging.LoadState
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
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.api.FosdemApi
|
||||||
|
import be.digitalia.fosdem.utils.assistedViewModels
|
||||||
|
import be.digitalia.fosdem.utils.launchAndRepeatOnLifecycle
|
||||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
@ -26,12 +33,15 @@ class ExternalBookmarksListFragment : Fragment(R.layout.recyclerview) {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var api: FosdemApi
|
lateinit var api: FosdemApi
|
||||||
private val viewModel: ExternalBookmarksViewModel by viewModels()
|
@Inject
|
||||||
private var addAllMenuItem: MenuItem? = null
|
lateinit var viewModelFactory: ExternalBookmarksViewModel.Factory
|
||||||
|
private val viewModel: ExternalBookmarksViewModel by assistedViewModels {
|
||||||
|
val bookmarkIds = requireArguments().getLongArray(ARG_BOOKMARK_IDS)!!
|
||||||
|
viewModelFactory.create(bookmarkIds)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setHasOptionsMenu(true)
|
|
||||||
setFragmentResultListener(REQUEST_KEY_CONFIRM_ADD_ALL) { _, _ -> viewModel.addAll() }
|
setFragmentResultListener(REQUEST_KEY_CONFIRM_ADD_ALL) { _, _ -> viewModel.addAll() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,42 +59,42 @@ class ExternalBookmarksListFragment : Fragment(R.layout.recyclerview) {
|
||||||
isProgressBarVisible = true
|
isProgressBarVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val bookmarkIds = requireArguments().getLongArray(ARG_BOOKMARK_IDS)!!
|
val menuProvider = object : MenuProvider {
|
||||||
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
api.roomStatuses.observe(viewLifecycleOwner) { statuses ->
|
menuInflater.inflate(R.menu.external_bookmarks, menu)
|
||||||
adapter.roomStatuses = statuses
|
|
||||||
}
|
|
||||||
with(viewModel) {
|
|
||||||
setBookmarkIds(bookmarkIds)
|
|
||||||
bookmarks.observe(viewLifecycleOwner) { bookmarks ->
|
|
||||||
adapter.submitList(bookmarks)
|
|
||||||
addAllMenuItem?.isEnabled = bookmarks.isNotEmpty()
|
|
||||||
holder.isProgressBarVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onMenuItemSelected(menuItem: MenuItem) = when (menuItem.itemId) {
|
||||||
inflater.inflate(R.menu.external_bookmarks, menu)
|
|
||||||
menu.findItem(R.id.add_all)?.let { item ->
|
|
||||||
val bookmarks = viewModel.bookmarks.value
|
|
||||||
item.isEnabled = bookmarks != null && bookmarks.isNotEmpty()
|
|
||||||
addAllMenuItem = item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyOptionsMenu() {
|
|
||||||
super.onDestroyOptionsMenu()
|
|
||||||
addAllMenuItem = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
|
||||||
R.id.add_all -> {
|
R.id.add_all -> {
|
||||||
ConfirmAddAllDialogFragment().show(parentFragmentManager, "confirmAddAll")
|
ConfirmAddAllDialogFragment().show(parentFragmentManager, "confirmAddAll")
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
adapter.loadStateFlow.first { it.refresh !is LoadState.Loading }
|
||||||
|
holder.isProgressBarVisible = false
|
||||||
|
// Only display the menu items if there is at least one item
|
||||||
|
if (adapter.itemCount > 0) {
|
||||||
|
requireActivity().addMenuProvider(menuProvider, viewLifecycleOwner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewLifecycleOwner.launchAndRepeatOnLifecycle {
|
||||||
|
launch {
|
||||||
|
api.roomStatuses.collect { statuses ->
|
||||||
|
adapter.roomStatuses = statuses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
viewModel.bookmarks.collectLatest { pagingData ->
|
||||||
|
adapter.submitData(pagingData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ConfirmAddAllDialogFragment : DialogFragment() {
|
class ConfirmAddAllDialogFragment : DialogFragment() {
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
|
@ -5,22 +5,30 @@ import android.view.View
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.LoadState
|
||||||
|
import androidx.paging.PagingData
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
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.api.FosdemApi
|
||||||
import be.digitalia.fosdem.model.StatusEvent
|
import be.digitalia.fosdem.model.StatusEvent
|
||||||
|
import be.digitalia.fosdem.utils.launchAndRepeatOnLifecycle
|
||||||
import be.digitalia.fosdem.viewmodels.LiveViewModel
|
import be.digitalia.fosdem.viewmodels.LiveViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
sealed class LiveListFragment(@StringRes private val emptyTextResId: Int,
|
sealed class LiveListFragment(
|
||||||
private val dataSourceProvider: (LiveViewModel) -> LiveData<PagedList<StatusEvent>>)
|
@StringRes private val emptyTextResId: Int,
|
||||||
: Fragment(R.layout.recyclerview) {
|
private val dataSourceProvider: (LiveViewModel) -> Flow<PagingData<StatusEvent>>
|
||||||
|
) : Fragment(R.layout.recyclerview) {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var api: FosdemApi
|
lateinit var api: FosdemApi
|
||||||
|
@ -45,20 +53,31 @@ sealed class LiveListFragment(@StringRes private val emptyTextResId: Int,
|
||||||
isProgressBarVisible = true
|
isProgressBarVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
api.roomStatuses.observe(viewLifecycleOwner) { statuses ->
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
adapter.loadStateFlow
|
||||||
|
.distinctUntilChangedBy { it.refresh }
|
||||||
|
.filter { it.refresh !is LoadState.Loading }
|
||||||
|
.collect {
|
||||||
|
holder.isProgressBarVisible = false
|
||||||
|
// Ensure we stay at scroll position 0 so we can see the insertion animation
|
||||||
|
with(holder.recyclerView) {
|
||||||
|
if (scrollY == 0) scrollToPosition(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewLifecycleOwner.launchAndRepeatOnLifecycle {
|
||||||
|
launch {
|
||||||
|
api.roomStatuses.collect { statuses ->
|
||||||
adapter.roomStatuses = statuses
|
adapter.roomStatuses = statuses
|
||||||
}
|
}
|
||||||
dataSourceProvider(viewModel).observe(viewLifecycleOwner) { events ->
|
}
|
||||||
adapter.submitList(events) {
|
launch {
|
||||||
// Ensure we stay at scroll position 0 so we can see the insertion animation
|
dataSourceProvider(viewModel).collectLatest { pagingData ->
|
||||||
holder.recyclerView.run {
|
adapter.submitData(pagingData)
|
||||||
if (scrollY == 0) {
|
|
||||||
scrollToPosition(0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
holder.isProgressBarVisible = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,8 @@ 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.lifecycle.lifecycleScope
|
||||||
|
import androidx.paging.LoadState
|
||||||
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
|
||||||
|
@ -13,8 +14,13 @@ 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.api.FosdemApi
|
||||||
import be.digitalia.fosdem.model.Person
|
import be.digitalia.fosdem.model.Person
|
||||||
|
import be.digitalia.fosdem.utils.assistedViewModels
|
||||||
|
import be.digitalia.fosdem.utils.launchAndRepeatOnLifecycle
|
||||||
import be.digitalia.fosdem.viewmodels.PersonInfoViewModel
|
import be.digitalia.fosdem.viewmodels.PersonInfoViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
@ -22,14 +28,16 @@ class PersonInfoListFragment : Fragment(R.layout.recyclerview) {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var api: FosdemApi
|
lateinit var api: FosdemApi
|
||||||
private val viewModel: PersonInfoViewModel by viewModels()
|
@Inject
|
||||||
|
lateinit var viewModelFactory: PersonInfoViewModel.Factory
|
||||||
|
private val viewModel: PersonInfoViewModel by assistedViewModels {
|
||||||
|
val person: Person = requireArguments().getParcelable(ARG_PERSON)!!
|
||||||
|
viewModelFactory.create(person)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
val person: Person = requireArguments().getParcelable(ARG_PERSON)!!
|
|
||||||
viewModel.setPerson(person)
|
|
||||||
|
|
||||||
val adapter = EventsAdapter(view.context)
|
val adapter = EventsAdapter(view.context)
|
||||||
val holder = RecyclerViewViewHolder(view).apply {
|
val holder = RecyclerViewViewHolder(view).apply {
|
||||||
recyclerView.apply {
|
recyclerView.apply {
|
||||||
|
@ -47,12 +55,22 @@ class PersonInfoListFragment : Fragment(R.layout.recyclerview) {
|
||||||
isProgressBarVisible = true
|
isProgressBarVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
api.roomStatuses.observe(viewLifecycleOwner) { statuses ->
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
adapter.loadStateFlow.first { it.refresh !is LoadState.Loading }
|
||||||
|
holder.isProgressBarVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
viewLifecycleOwner.launchAndRepeatOnLifecycle {
|
||||||
|
launch {
|
||||||
|
api.roomStatuses.collect { statuses ->
|
||||||
adapter.roomStatuses = statuses
|
adapter.roomStatuses = statuses
|
||||||
}
|
}
|
||||||
viewModel.events.observe(viewLifecycleOwner) { events ->
|
}
|
||||||
adapter.submitList(events)
|
launch {
|
||||||
holder.isProgressBarVisible = false
|
viewModel.events.collectLatest { pagingData ->
|
||||||
|
adapter.submitData(pagingData)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,9 @@ import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.paging.PagedListAdapter
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.paging.LoadState
|
||||||
|
import androidx.paging.PagingDataAdapter
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -16,8 +18,12 @@ import be.digitalia.fosdem.R
|
||||||
import be.digitalia.fosdem.activities.PersonInfoActivity
|
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.utils.launchAndRepeatOnLifecycle
|
||||||
import be.digitalia.fosdem.viewmodels.PersonsViewModel
|
import be.digitalia.fosdem.viewmodels.PersonsViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class PersonsListFragment : Fragment(R.layout.recyclerview_fastscroll) {
|
class PersonsListFragment : Fragment(R.layout.recyclerview_fastscroll) {
|
||||||
|
@ -38,13 +44,19 @@ class PersonsListFragment : Fragment(R.layout.recyclerview_fastscroll) {
|
||||||
isProgressBarVisible = true
|
isProgressBarVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.persons.observe(viewLifecycleOwner) { persons ->
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
adapter.submitList(persons)
|
adapter.loadStateFlow.first { it.refresh !is LoadState.Loading }
|
||||||
holder.isProgressBarVisible = false
|
holder.isProgressBarVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewLifecycleOwner.launchAndRepeatOnLifecycle {
|
||||||
|
viewModel.persons.collectLatest { pagingData ->
|
||||||
|
adapter.submitData(pagingData)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PersonsAdapter : PagedListAdapter<Person, PersonViewHolder>(DIFF_CALLBACK) {
|
private class PersonsAdapter : PagingDataAdapter<Person, PersonViewHolder>(DIFF_CALLBACK) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PersonViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PersonViewHolder {
|
||||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.simple_list_item_1_material, parent, false)
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.simple_list_item_1_material, parent, false)
|
||||||
|
|
|
@ -4,13 +4,19 @@ import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.paging.LoadState
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
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.api.FosdemApi
|
||||||
|
import be.digitalia.fosdem.utils.launchAndRepeatOnLifecycle
|
||||||
import be.digitalia.fosdem.viewmodels.SearchViewModel
|
import be.digitalia.fosdem.viewmodels.SearchViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
@ -34,12 +40,22 @@ class SearchResultListFragment : Fragment(R.layout.recyclerview) {
|
||||||
isProgressBarVisible = true
|
isProgressBarVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
api.roomStatuses.observe(viewLifecycleOwner) { statuses ->
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
adapter.roomStatuses = statuses
|
adapter.loadStateFlow.first { it.refresh !is LoadState.Loading }
|
||||||
}
|
|
||||||
viewModel.results.observe(viewLifecycleOwner) { result ->
|
|
||||||
adapter.submitList((result as? SearchViewModel.Result.Success)?.list)
|
|
||||||
holder.isProgressBarVisible = false
|
holder.isProgressBarVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewLifecycleOwner.launchAndRepeatOnLifecycle {
|
||||||
|
launch {
|
||||||
|
api.roomStatuses.collect { statuses ->
|
||||||
|
adapter.roomStatuses = statuses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
viewModel.results.collectLatest { pagingData ->
|
||||||
|
adapter.submitData(pagingData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,7 +5,6 @@ import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -15,14 +14,25 @@ import be.digitalia.fosdem.adapters.TrackScheduleAdapter
|
||||||
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 be.digitalia.fosdem.utils.assistedViewModels
|
||||||
|
import be.digitalia.fosdem.utils.launchAndRepeatOnLifecycle
|
||||||
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
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class TrackScheduleListFragment : Fragment(R.layout.recyclerview), TrackScheduleAdapter.EventClickListener {
|
class TrackScheduleListFragment : Fragment(R.layout.recyclerview), TrackScheduleAdapter.EventClickListener {
|
||||||
|
|
||||||
private val viewModel: TrackScheduleListViewModel by viewModels()
|
@Inject
|
||||||
|
lateinit var viewModelFactory: TrackScheduleListViewModel.Factory
|
||||||
|
private val viewModel: TrackScheduleListViewModel by assistedViewModels {
|
||||||
|
val args = requireArguments()
|
||||||
|
val day: Day = args.getParcelable(ARG_DAY)!!
|
||||||
|
val track: Track = args.getParcelable(ARG_TRACK)!!
|
||||||
|
viewModelFactory.create(day, track)
|
||||||
|
}
|
||||||
private val activityViewModel: TrackScheduleViewModel by activityViewModels()
|
private val activityViewModel: TrackScheduleViewModel by activityViewModels()
|
||||||
private val selectionEnabled: Boolean by lazy(LazyThreadSafetyMode.NONE) {
|
private val selectionEnabled: Boolean by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
resources.getBoolean(R.bool.tablet_landscape)
|
resources.getBoolean(R.bool.tablet_landscape)
|
||||||
|
@ -32,16 +42,11 @@ class TrackScheduleListFragment : Fragment(R.layout.recyclerview), TrackSchedule
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val args = requireArguments()
|
|
||||||
val day: Day = args.getParcelable(ARG_DAY)!!
|
|
||||||
val track: Track = args.getParcelable(ARG_TRACK)!!
|
|
||||||
viewModel.setDayAndTrack(day, track)
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
isListAlreadyShown = savedInstanceState.getBoolean(STATE_IS_LIST_ALREADY_SHOWN)
|
isListAlreadyShown = savedInstanceState.getBoolean(STATE_IS_LIST_ALREADY_SHOWN)
|
||||||
}
|
}
|
||||||
selectedId = savedInstanceState?.getLong(STATE_SELECTED_ID)
|
selectedId = savedInstanceState?.getLong(STATE_SELECTED_ID)
|
||||||
?: args.getLong(ARG_FROM_EVENT_ID, RecyclerView.NO_ID)
|
?: requireArguments().getLong(ARG_FROM_EVENT_ID, RecyclerView.NO_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var selectedId: Long = RecyclerView.NO_ID
|
private var selectedId: Long = RecyclerView.NO_ID
|
||||||
|
@ -66,11 +71,9 @@ class TrackScheduleListFragment : Fragment(R.layout.recyclerview), TrackSchedule
|
||||||
isProgressBarVisible = true
|
isProgressBarVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
with(viewModel) {
|
viewLifecycleOwner.launchAndRepeatOnLifecycle {
|
||||||
currentTime.observe(viewLifecycleOwner) { now ->
|
launch {
|
||||||
adapter.currentTime = now
|
viewModel.schedule.collect { schedule ->
|
||||||
}
|
|
||||||
schedule.observe(viewLifecycleOwner) { schedule ->
|
|
||||||
adapter.submitList(schedule)
|
adapter.submitList(schedule)
|
||||||
|
|
||||||
var selectedPosition = if (selectedId == -1L) -1 else schedule.indexOfFirst { it.event.id == selectedId }
|
var selectedPosition = if (selectedId == -1L) -1 else schedule.indexOfFirst { it.event.id == selectedId }
|
||||||
|
@ -84,7 +87,8 @@ class TrackScheduleListFragment : Fragment(R.layout.recyclerview), TrackSchedule
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
activityViewModel.setSelectEvent(if (selectedPosition == -1) null else schedule[selectedPosition].event)
|
activityViewModel.selectedEvent =
|
||||||
|
if (selectedPosition == -1) null else schedule[selectedPosition].event
|
||||||
|
|
||||||
// Ensure the selection is visible
|
// Ensure the selection is visible
|
||||||
if ((selectionEnabled || !isListAlreadyShown) && selectedPosition != -1) {
|
if ((selectionEnabled || !isListAlreadyShown) && selectedPosition != -1) {
|
||||||
|
@ -95,16 +99,26 @@ class TrackScheduleListFragment : Fragment(R.layout.recyclerview), TrackSchedule
|
||||||
holder.isProgressBarVisible = false
|
holder.isProgressBarVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
launch {
|
||||||
|
viewModel.currentTime.collect { now ->
|
||||||
|
adapter.currentTime = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (selectionEnabled) {
|
if (selectionEnabled) {
|
||||||
activityViewModel.selectedEvent.observe(viewLifecycleOwner) { event ->
|
launch {
|
||||||
|
activityViewModel.selectedEventFlow.collect { event ->
|
||||||
adapter.selectedId = event?.id ?: RecyclerView.NO_ID
|
adapter.selectedId = event?.id ?: RecyclerView.NO_ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onEventClick(event: Event) {
|
override fun onEventClick(event: Event) {
|
||||||
selectedId = event.id
|
selectedId = event.id
|
||||||
activityViewModel.setSelectEvent(event)
|
activityViewModel.selectedEvent = event
|
||||||
|
|
||||||
if (!selectionEnabled) {
|
if (!selectionEnabled) {
|
||||||
// Classic mode: Show event details in a new activity
|
// Classic mode: Show event details in a new activity
|
||||||
|
|
|
@ -16,6 +16,7 @@ import be.digitalia.fosdem.R
|
||||||
import be.digitalia.fosdem.model.Day
|
import be.digitalia.fosdem.model.Day
|
||||||
import be.digitalia.fosdem.utils.enforceSingleScrollDirection
|
import be.digitalia.fosdem.utils.enforceSingleScrollDirection
|
||||||
import be.digitalia.fosdem.utils.instantiate
|
import be.digitalia.fosdem.utils.instantiate
|
||||||
|
import be.digitalia.fosdem.utils.launchAndRepeatOnLifecycle
|
||||||
import be.digitalia.fosdem.utils.recyclerView
|
import be.digitalia.fosdem.utils.recyclerView
|
||||||
import be.digitalia.fosdem.utils.viewLifecycleLazy
|
import be.digitalia.fosdem.utils.viewLifecycleLazy
|
||||||
import be.digitalia.fosdem.viewmodels.TracksViewModel
|
import be.digitalia.fosdem.viewmodels.TracksViewModel
|
||||||
|
@ -57,12 +58,12 @@ class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvi
|
||||||
preferences.getInt(TRACKS_CURRENT_PAGE_PREF_KEY, -1)
|
preferences.getInt(TRACKS_CURRENT_PAGE_PREF_KEY, -1)
|
||||||
} else -1
|
} else -1
|
||||||
|
|
||||||
viewModel.days.observe(viewLifecycleOwner) { days ->
|
viewLifecycleOwner.launchAndRepeatOnLifecycle {
|
||||||
|
viewModel.days.collect { days ->
|
||||||
holder.run {
|
holder.run {
|
||||||
daysAdapter.days = days
|
daysAdapter.days = days
|
||||||
|
|
||||||
val totalPages = daysAdapter.itemCount
|
if (days.isEmpty()) {
|
||||||
if (totalPages == 0) {
|
|
||||||
contentView.isVisible = false
|
contentView.isVisible = false
|
||||||
emptyView.isVisible = true
|
emptyView.isVisible = true
|
||||||
} else {
|
} else {
|
||||||
|
@ -73,12 +74,13 @@ class TracksFragment : Fragment(R.layout.fragment_tracks), RecycledViewPoolProvi
|
||||||
TabLayoutMediator(tabs, pager) { tab, position -> tab.text = daysAdapter.getPageTitle(position) }.attach()
|
TabLayoutMediator(tabs, pager) { tab, position -> tab.text = daysAdapter.getPageTitle(position) }.attach()
|
||||||
}
|
}
|
||||||
if (savedCurrentPage != -1) {
|
if (savedCurrentPage != -1) {
|
||||||
pager.setCurrentItem(savedCurrentPage.coerceAtMost(totalPages - 1), false)
|
pager.setCurrentItem(savedCurrentPage.coerceAtMost(days.size - 1), false)
|
||||||
savedCurrentPage = -1
|
savedCurrentPage = -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
|
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
|
||||||
if (event == Lifecycle.Event.ON_STOP) {
|
if (event == Lifecycle.Event.ON_STOP) {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
@ -18,13 +17,21 @@ import be.digitalia.fosdem.R
|
||||||
import be.digitalia.fosdem.activities.TrackScheduleActivity
|
import be.digitalia.fosdem.activities.TrackScheduleActivity
|
||||||
import be.digitalia.fosdem.model.Day
|
import be.digitalia.fosdem.model.Day
|
||||||
import be.digitalia.fosdem.model.Track
|
import be.digitalia.fosdem.model.Track
|
||||||
|
import be.digitalia.fosdem.utils.assistedViewModels
|
||||||
|
import be.digitalia.fosdem.utils.launchAndRepeatOnLifecycle
|
||||||
import be.digitalia.fosdem.viewmodels.TracksListViewModel
|
import be.digitalia.fosdem.viewmodels.TracksListViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class TracksListFragment : Fragment(R.layout.recyclerview) {
|
class TracksListFragment : Fragment(R.layout.recyclerview) {
|
||||||
|
|
||||||
private val viewModel: TracksListViewModel by viewModels()
|
@Inject
|
||||||
|
lateinit var viewModelFactory: TracksListViewModel.Factory
|
||||||
|
private val viewModel: TracksListViewModel by assistedViewModels {
|
||||||
|
viewModelFactory.create(day)
|
||||||
|
}
|
||||||
|
|
||||||
private val day by lazy<Day>(LazyThreadSafetyMode.NONE) {
|
private val day by lazy<Day>(LazyThreadSafetyMode.NONE) {
|
||||||
requireArguments().getParcelable(ARG_DAY)!!
|
requireArguments().getParcelable(ARG_DAY)!!
|
||||||
}
|
}
|
||||||
|
@ -48,9 +55,8 @@ class TracksListFragment : Fragment(R.layout.recyclerview) {
|
||||||
isProgressBarVisible = true
|
isProgressBarVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
with(viewModel) {
|
viewLifecycleOwner.launchAndRepeatOnLifecycle {
|
||||||
setDay(day)
|
viewModel.tracks.collect { tracks ->
|
||||||
tracks.observe(viewLifecycleOwner) { tracks ->
|
|
||||||
adapter.submitList(tracks)
|
adapter.submitList(tracks)
|
||||||
holder.isProgressBarVisible = false
|
holder.isProgressBarVisible = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
package be.digitalia.fosdem.livedata
|
|
||||||
|
|
||||||
import android.os.Looper
|
|
||||||
import android.os.SystemClock
|
|
||||||
import androidx.core.os.HandlerCompat
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import java.util.Arrays
|
|
||||||
|
|
||||||
object LiveDataFactory {
|
|
||||||
|
|
||||||
private val handler = HandlerCompat.createAsync(Looper.getMainLooper())
|
|
||||||
|
|
||||||
fun interval(periodInMillis: Long): LiveData<Long> {
|
|
||||||
return IntervalLiveData(periodInMillis)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a LiveData whose value is true during scheduled periods.
|
|
||||||
*
|
|
||||||
* @param startEndTimestamps a list of timestamps in milliseconds, sorted in chronological order.
|
|
||||||
* Odd and even values represent beginnings and ends of periods, respectively.
|
|
||||||
*/
|
|
||||||
fun scheduler(vararg startEndTimestamps: Long): LiveData<Boolean> {
|
|
||||||
return SchedulerLiveData(startEndTimestamps)
|
|
||||||
}
|
|
||||||
|
|
||||||
private class IntervalLiveData(private val periodInMillis: Long) : LiveData<Long>(), Runnable {
|
|
||||||
|
|
||||||
private var updateTime = 0L
|
|
||||||
private var version = 0L
|
|
||||||
|
|
||||||
override fun onActive() {
|
|
||||||
val now = SystemClock.elapsedRealtime()
|
|
||||||
if (now >= updateTime) {
|
|
||||||
update(now)
|
|
||||||
} else {
|
|
||||||
handler.postDelayed(this, updateTime - now)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInactive() {
|
|
||||||
handler.removeCallbacks(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun update(now: Long) {
|
|
||||||
value = version++
|
|
||||||
updateTime = now + periodInMillis
|
|
||||||
handler.postDelayed(this, periodInMillis)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
update(SystemClock.elapsedRealtime())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SchedulerLiveData(private val startEndTimestamps: LongArray) : LiveData<Boolean>(), Runnable {
|
|
||||||
|
|
||||||
private var nowPosition = -1
|
|
||||||
|
|
||||||
override fun onActive() {
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
updateState(now, Arrays.binarySearch(startEndTimestamps, now))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInactive() {
|
|
||||||
handler.removeCallbacks(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
val position = nowPosition
|
|
||||||
updateState(startEndTimestamps[position], position)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateState(now: Long, position: Int) {
|
|
||||||
var pos = position
|
|
||||||
val size = startEndTimestamps.size
|
|
||||||
if (pos >= 0) {
|
|
||||||
do {
|
|
||||||
pos++
|
|
||||||
} while (pos < size && startEndTimestamps[pos] == now)
|
|
||||||
} else {
|
|
||||||
pos = pos.inv()
|
|
||||||
}
|
|
||||||
val isOn = pos % 2 != 0
|
|
||||||
if (value != isOn) {
|
|
||||||
value = isOn
|
|
||||||
}
|
|
||||||
if (pos < size) {
|
|
||||||
nowPosition = pos
|
|
||||||
handler.postDelayed(this, startEndTimestamps[pos] - now)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package be.digitalia.fosdem.livedata
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encapsulates data that can only be consumed once.
|
|
||||||
*/
|
|
||||||
class SingleEvent<T :Any>(content: T) {
|
|
||||||
|
|
||||||
private var content: T? = content
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The content, or null if it has already been consumed.
|
|
||||||
*/
|
|
||||||
fun consume(): T? {
|
|
||||||
val previousContent = content
|
|
||||||
if (previousContent != null) {
|
|
||||||
content = null
|
|
||||||
}
|
|
||||||
return previousContent
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +1,3 @@
|
||||||
package be.digitalia.fosdem.model
|
package be.digitalia.fosdem.model
|
||||||
|
|
||||||
class BookmarkStatus(val isBookmarked: Boolean, val isUpdate: Boolean)
|
data class BookmarkStatus(val eventId: Long, val isBookmarked: Boolean)
|
|
@ -1,7 +1,7 @@
|
||||||
package be.digitalia.fosdem.model
|
package be.digitalia.fosdem.model
|
||||||
|
|
||||||
sealed class DownloadScheduleResult {
|
sealed class DownloadScheduleResult {
|
||||||
class Success(val eventsCount: Int) : DownloadScheduleResult()
|
data class Success(val eventsCount: Int) : DownloadScheduleResult()
|
||||||
object Error : DownloadScheduleResult()
|
object Error : DownloadScheduleResult()
|
||||||
object UpToDate : DownloadScheduleResult()
|
object UpToDate : DownloadScheduleResult()
|
||||||
}
|
}
|
|
@ -1,13 +1,11 @@
|
||||||
package be.digitalia.fosdem.model
|
package be.digitalia.fosdem.model
|
||||||
|
|
||||||
import be.digitalia.fosdem.livedata.SingleEvent
|
|
||||||
|
|
||||||
sealed class LoadingState<out T : Any> {
|
sealed class LoadingState<out T : Any> {
|
||||||
/**
|
/**
|
||||||
* The current download progress:
|
* The current download progress:
|
||||||
* -1 : in progress, indeterminate
|
* -1 : in progress, indeterminate
|
||||||
* 0..99: progress value in percents
|
* 0..99: progress value in percents
|
||||||
*/
|
*/
|
||||||
class Loading(val progress: Int = -1) : LoadingState<Nothing>()
|
data class Loading(val progress: Int = -1) : LoadingState<Nothing>()
|
||||||
class Idle<T : Any>(val result: SingleEvent<T>) : LoadingState<T>()
|
data class Idle<T : Any>(val result: T? = null) : LoadingState<T>()
|
||||||
}
|
}
|
|
@ -105,7 +105,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 = bookmarksDao.getBookmarks()
|
val bookmarks = runBlocking { 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")
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package be.digitalia.fosdem.utils
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.annotation.MainThread
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.ViewModelStoreOwner
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
inline fun simpleViewModelProviderFactory(crossinline viewModelProducer: () -> ViewModel): ViewModelProvider.Factory {
|
||||||
|
return object : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>) = viewModelProducer() as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
inline fun <reified VM : ViewModel> ComponentActivity.assistedViewModels(
|
||||||
|
crossinline viewModelProducer: () -> VM
|
||||||
|
) = viewModels<VM> {
|
||||||
|
simpleViewModelProviderFactory(viewModelProducer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
inline fun <reified VM : ViewModel> Fragment.assistedViewModels(
|
||||||
|
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
|
||||||
|
crossinline viewModelProducer: () -> VM
|
||||||
|
) = viewModels<VM>(ownerProducer) {
|
||||||
|
simpleViewModelProviderFactory(viewModelProducer)
|
||||||
|
}
|
21
app/src/main/java/be/digitalia/fosdem/utils/LifecycleExt.kt
Normal file
21
app/src/main/java/be/digitalia/fosdem/utils/LifecycleExt.kt
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package be.digitalia.fosdem.utils
|
||||||
|
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.coroutineScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
fun Lifecycle.launchAndRepeatOnLifecycle(
|
||||||
|
state: Lifecycle.State = Lifecycle.State.STARTED,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
): Job = coroutineScope.launch {
|
||||||
|
repeatOnLifecycle(state, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LifecycleOwner.launchAndRepeatOnLifecycle(
|
||||||
|
state: Lifecycle.State = Lifecycle.State.STARTED,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
): Job = lifecycle.launchAndRepeatOnLifecycle(state, block)
|
|
@ -1,17 +1,20 @@
|
||||||
package be.digitalia.fosdem.viewmodels
|
package be.digitalia.fosdem.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.distinctUntilChanged
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.map
|
|
||||||
import androidx.lifecycle.switchMap
|
|
||||||
import be.digitalia.fosdem.alarms.AppAlarmManager
|
import be.digitalia.fosdem.alarms.AppAlarmManager
|
||||||
import be.digitalia.fosdem.db.BookmarksDao
|
import be.digitalia.fosdem.db.BookmarksDao
|
||||||
|
import be.digitalia.fosdem.flow.stateFlow
|
||||||
|
import be.digitalia.fosdem.flow.versionedResourceFlow
|
||||||
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 be.digitalia.fosdem.utils.BackgroundWorkScope
|
import be.digitalia.fosdem.utils.BackgroundWorkScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -21,37 +24,34 @@ class BookmarkStatusViewModel @Inject constructor(
|
||||||
private val alarmManager: AppAlarmManager
|
private val alarmManager: AppAlarmManager
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val eventLiveData = MutableLiveData<Event?>()
|
private val eventStateFlow = MutableStateFlow<Event?>(null)
|
||||||
private var firstResultReceived = false
|
|
||||||
|
|
||||||
val bookmarkStatus: LiveData<BookmarkStatus?> = eventLiveData.switchMap { event ->
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val bookmarkStatus: StateFlow<BookmarkStatus?> =
|
||||||
|
stateFlow(viewModelScope, null) { subscriptionCount ->
|
||||||
|
eventStateFlow.flatMapLatest { event ->
|
||||||
if (event == null) {
|
if (event == null) {
|
||||||
MutableLiveData(null)
|
flowOf(null)
|
||||||
} else {
|
} else {
|
||||||
bookmarksDao.getBookmarkStatus(event)
|
versionedResourceFlow(bookmarksDao.version, subscriptionCount) {
|
||||||
.distinctUntilChanged() // Prevent updating the UI when a bookmark is added back or removed back
|
val isBookmarked = bookmarksDao.getBookmarkStatus(event)
|
||||||
.map { isBookmarked ->
|
BookmarkStatus(event.id, isBookmarked)
|
||||||
val isUpdate = firstResultReceived
|
}
|
||||||
firstResultReceived = true
|
|
||||||
BookmarkStatus(isBookmarked, isUpdate)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var event: Event?
|
var event: Event?
|
||||||
get() = eventLiveData.value
|
get() = eventStateFlow.value
|
||||||
set(value) {
|
set(value) {
|
||||||
if (value != eventLiveData.value) {
|
eventStateFlow.value = value
|
||||||
firstResultReceived = false
|
|
||||||
eventLiveData.value = value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleBookmarkStatus() {
|
fun toggleBookmarkStatus() {
|
||||||
val event = event
|
val event = eventStateFlow.value
|
||||||
val currentStatus = bookmarkStatus.value
|
val currentStatus = bookmarkStatus.value
|
||||||
// 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 && event.id == currentStatus.eventId) {
|
||||||
if (currentStatus.isBookmarked) {
|
if (currentStatus.isBookmarked) {
|
||||||
removeBookmark(event)
|
removeBookmark(event)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -2,20 +2,28 @@ package be.digitalia.fosdem.viewmodels
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.switchMap
|
import androidx.lifecycle.viewModelScope
|
||||||
import be.digitalia.fosdem.BuildConfig
|
import be.digitalia.fosdem.BuildConfig
|
||||||
import be.digitalia.fosdem.alarms.AppAlarmManager
|
import be.digitalia.fosdem.alarms.AppAlarmManager
|
||||||
import be.digitalia.fosdem.db.BookmarksDao
|
import be.digitalia.fosdem.db.BookmarksDao
|
||||||
import be.digitalia.fosdem.db.ScheduleDao
|
import be.digitalia.fosdem.db.ScheduleDao
|
||||||
import be.digitalia.fosdem.livedata.LiveDataFactory
|
import be.digitalia.fosdem.flow.flowWhileShared
|
||||||
|
import be.digitalia.fosdem.flow.rememberTickerFlow
|
||||||
|
import be.digitalia.fosdem.flow.stateFlow
|
||||||
|
import be.digitalia.fosdem.flow.versionedResourceFlow
|
||||||
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 be.digitalia.fosdem.utils.BackgroundWorkScope
|
import be.digitalia.fosdem.utils.BackgroundWorkScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
|
@ -33,26 +41,35 @@ class BookmarksViewModel @Inject constructor(
|
||||||
private val application: Application
|
private val application: Application
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val upcomingOnlyLiveData = MutableLiveData<Boolean>()
|
private val upcomingOnlyStateFlow = MutableStateFlow<Boolean?>(null)
|
||||||
|
|
||||||
val bookmarks: LiveData<List<Event>> = upcomingOnlyLiveData.switchMap { upcomingOnly: Boolean ->
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val bookmarks: StateFlow<List<Event>?> = stateFlow(viewModelScope, null) { subscriptionCount ->
|
||||||
|
upcomingOnlyStateFlow.filterNotNull().flatMapLatest { upcomingOnly ->
|
||||||
if (upcomingOnly) {
|
if (upcomingOnly) {
|
||||||
// Refresh upcoming bookmarks every 2 minutes
|
// Refresh upcoming bookmarks every 2 minutes
|
||||||
LiveDataFactory.interval(REFRESH_PERIOD)
|
rememberTickerFlow(REFRESH_PERIOD)
|
||||||
.switchMap {
|
.flowWhileShared(subscriptionCount, SharingStarted.WhileSubscribed())
|
||||||
bookmarksDao.getBookmarks(Instant.now() - TIME_OFFSET)
|
.flatMapLatest {
|
||||||
|
getObservableBookmarks(Instant.now() - TIME_OFFSET, subscriptionCount)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bookmarksDao.getBookmarks(Instant.EPOCH)
|
getObservableBookmarks(Instant.EPOCH, subscriptionCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getObservableBookmarks(
|
||||||
|
minStartTime: Instant,
|
||||||
|
subscriptionCount: StateFlow<Int>
|
||||||
|
): Flow<List<Event>> = versionedResourceFlow(bookmarksDao.version, subscriptionCount) {
|
||||||
|
bookmarksDao.getBookmarks(minStartTime)
|
||||||
|
}
|
||||||
|
|
||||||
var upcomingOnly: Boolean
|
var upcomingOnly: Boolean
|
||||||
get() = upcomingOnlyLiveData.value == true
|
get() = upcomingOnlyStateFlow.value == true
|
||||||
set(value) {
|
set(value) {
|
||||||
if (value != upcomingOnlyLiveData.value) {
|
upcomingOnlyStateFlow.value = value
|
||||||
upcomingOnlyLiveData.value = value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeBookmarks(eventIds: LongArray) {
|
fun removeBookmarks(eventIds: LongArray) {
|
||||||
|
@ -73,6 +90,7 @@ class BookmarksViewModel @Inject constructor(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val REFRESH_PERIOD = TimeUnit.MINUTES.toMillis(2L)
|
private val REFRESH_PERIOD = TimeUnit.MINUTES.toMillis(2L)
|
||||||
|
|
||||||
// In upcomingOnly mode, events that just started are still shown for 5 minutes
|
// In upcomingOnly mode, events that just started are still shown for 5 minutes
|
||||||
private val TIME_OFFSET = Duration.ofMinutes(5L)
|
private val TIME_OFFSET = Duration.ofMinutes(5L)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
package be.digitalia.fosdem.viewmodels
|
package be.digitalia.fosdem.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.switchMap
|
import androidx.lifecycle.viewModelScope
|
||||||
import be.digitalia.fosdem.db.ScheduleDao
|
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 dagger.assisted.Assisted
|
||||||
import javax.inject.Inject
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
|
||||||
@HiltViewModel
|
class EventDetailsViewModel @AssistedInject constructor(
|
||||||
class EventDetailsViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
scheduleDao: ScheduleDao,
|
||||||
|
@Assisted event: Event
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
private val eventLiveData = MutableLiveData<Event>()
|
val eventDetails: Deferred<EventDetails> = viewModelScope.async {
|
||||||
|
|
||||||
val eventDetails: LiveData<EventDetails> = eventLiveData.switchMap { event: Event ->
|
|
||||||
scheduleDao.getEventDetails(event)
|
scheduleDao.getEventDetails(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setEvent(event: Event) {
|
@AssistedFactory
|
||||||
if (event != eventLiveData.value) {
|
interface Factory {
|
||||||
eventLiveData.value = event
|
fun create(event: Event): EventDetailsViewModel
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,32 +1,26 @@
|
||||||
package be.digitalia.fosdem.viewmodels
|
package be.digitalia.fosdem.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.liveData
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.switchMap
|
|
||||||
import be.digitalia.fosdem.db.ScheduleDao
|
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 dagger.assisted.Assisted
|
||||||
import javax.inject.Inject
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
|
||||||
@HiltViewModel
|
class EventViewModel @AssistedInject constructor(
|
||||||
class EventViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
scheduleDao: ScheduleDao,
|
||||||
|
@Assisted eventId: Long
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
private val eventIdLiveData = MutableLiveData<Long>()
|
val event: Deferred<Event?> = viewModelScope.async {
|
||||||
|
scheduleDao.getEvent(eventId)
|
||||||
val event: LiveData<Event?> = eventIdLiveData.switchMap { id ->
|
|
||||||
liveData {
|
|
||||||
emit(scheduleDao.getEvent(id))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val isEventIdSet
|
@AssistedFactory
|
||||||
get() = eventIdLiveData.value != null
|
interface Factory {
|
||||||
|
fun create(eventId: Long): EventViewModel
|
||||||
fun setEventId(eventId: Long) {
|
|
||||||
if (eventId != eventIdLiveData.value) {
|
|
||||||
eventIdLiveData.value = eventId
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,46 +1,44 @@
|
||||||
package be.digitalia.fosdem.viewmodels
|
package be.digitalia.fosdem.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.switchMap
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.Pager
|
||||||
import androidx.paging.toLiveData
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.cachedIn
|
||||||
import be.digitalia.fosdem.alarms.AppAlarmManager
|
import be.digitalia.fosdem.alarms.AppAlarmManager
|
||||||
import be.digitalia.fosdem.db.BookmarksDao
|
import be.digitalia.fosdem.db.BookmarksDao
|
||||||
import be.digitalia.fosdem.db.ScheduleDao
|
import be.digitalia.fosdem.db.ScheduleDao
|
||||||
import be.digitalia.fosdem.model.StatusEvent
|
import be.digitalia.fosdem.model.StatusEvent
|
||||||
import be.digitalia.fosdem.utils.BackgroundWorkScope
|
import be.digitalia.fosdem.utils.BackgroundWorkScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
class ExternalBookmarksViewModel @AssistedInject constructor(
|
||||||
class ExternalBookmarksViewModel @Inject constructor(
|
|
||||||
scheduleDao: ScheduleDao,
|
scheduleDao: ScheduleDao,
|
||||||
private val bookmarksDao: BookmarksDao,
|
private val bookmarksDao: BookmarksDao,
|
||||||
private val alarmManager: AppAlarmManager
|
private val alarmManager: AppAlarmManager,
|
||||||
|
@Assisted private val bookmarkIds: LongArray
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val bookmarkIdsLiveData = MutableLiveData<LongArray>()
|
val bookmarks: Flow<PagingData<StatusEvent>> =
|
||||||
|
Pager(PagingConfig(20)) {
|
||||||
val bookmarks: LiveData<PagedList<StatusEvent>> = bookmarkIdsLiveData.switchMap { bookmarkIds ->
|
scheduleDao.getEvents(bookmarkIds)
|
||||||
scheduleDao.getEvents(bookmarkIds).toLiveData(20)
|
}.flow.cachedIn(viewModelScope)
|
||||||
}
|
|
||||||
|
|
||||||
fun setBookmarkIds(bookmarkIds: LongArray) {
|
|
||||||
val value = bookmarkIdsLiveData.value
|
|
||||||
if (value == null || !bookmarkIds.contentEquals(value)) {
|
|
||||||
bookmarkIdsLiveData.value = bookmarkIds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addAll() {
|
fun addAll() {
|
||||||
val bookmarkIds = bookmarkIdsLiveData.value ?: return
|
|
||||||
BackgroundWorkScope.launch {
|
BackgroundWorkScope.launch {
|
||||||
bookmarksDao.addBookmarks(bookmarkIds).let { alarmInfos ->
|
bookmarksDao.addBookmarks(bookmarkIds).let { alarmInfos ->
|
||||||
alarmManager.onBookmarksAdded(alarmInfos)
|
alarmManager.onBookmarksAdded(alarmInfos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(bookmarkIds: LongArray): ExternalBookmarksViewModel
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,14 +1,26 @@
|
||||||
package be.digitalia.fosdem.viewmodels
|
package be.digitalia.fosdem.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.switchMap
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.Pager
|
||||||
import androidx.paging.toLiveData
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.cachedIn
|
||||||
import be.digitalia.fosdem.db.ScheduleDao
|
import be.digitalia.fosdem.db.ScheduleDao
|
||||||
import be.digitalia.fosdem.livedata.LiveDataFactory
|
import be.digitalia.fosdem.flow.countSubscriptionsFlow
|
||||||
|
import be.digitalia.fosdem.flow.flowWhileShared
|
||||||
|
import be.digitalia.fosdem.flow.rememberTickerFlow
|
||||||
import be.digitalia.fosdem.model.StatusEvent
|
import be.digitalia.fosdem.model.StatusEvent
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -17,15 +29,33 @@ import javax.inject.Inject
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LiveViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
class LiveViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
||||||
|
|
||||||
private val heartbeat = LiveDataFactory.interval(REFRESH_PERIOD)
|
// Share a single ticker providing the time to ensure both lists are synchronized
|
||||||
|
private val ticker: Flow<Instant> =
|
||||||
|
rememberTickerFlow(REFRESH_PERIOD)
|
||||||
|
.map { Instant.now() }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
|
.filterNotNull()
|
||||||
|
|
||||||
val nextEvents: LiveData<PagedList<StatusEvent>> = heartbeat.switchMap {
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val now = Instant.now()
|
private fun createLiveEventsHotFlow(
|
||||||
scheduleDao.getEventsWithStartTime(now, now + NEXT_EVENTS_INTERVAL).toLiveData(20)
|
pagingSourceFactory: (now: Instant) -> PagingSource<Int, StatusEvent>
|
||||||
|
): Flow<PagingData<StatusEvent>> {
|
||||||
|
return countSubscriptionsFlow { subscriptionCount ->
|
||||||
|
ticker
|
||||||
|
.flowWhileShared(subscriptionCount, SharingStarted.WhileSubscribed())
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.flatMapLatest { now ->
|
||||||
|
Pager(PagingConfig(20)) { pagingSourceFactory(now) }.flow
|
||||||
|
}.cachedIn(viewModelScope)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val eventsInProgress: LiveData<PagedList<StatusEvent>> = heartbeat.switchMap {
|
val nextEvents: Flow<PagingData<StatusEvent>> = createLiveEventsHotFlow { now ->
|
||||||
scheduleDao.getEventsInProgress(Instant.now()).toLiveData(20)
|
scheduleDao.getEventsWithStartTime(now, now + NEXT_EVENTS_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
val eventsInProgress: Flow<PagingData<StatusEvent>> = createLiveEventsHotFlow { now ->
|
||||||
|
scheduleDao.getEventsInProgress(now)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -1,29 +1,30 @@
|
||||||
package be.digitalia.fosdem.viewmodels
|
package be.digitalia.fosdem.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.switchMap
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.Pager
|
||||||
import androidx.paging.toLiveData
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.cachedIn
|
||||||
import be.digitalia.fosdem.db.ScheduleDao
|
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 dagger.assisted.Assisted
|
||||||
import javax.inject.Inject
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@HiltViewModel
|
class PersonInfoViewModel @AssistedInject constructor(
|
||||||
class PersonInfoViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
scheduleDao: ScheduleDao,
|
||||||
|
@Assisted person: Person
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
private val personLiveData = MutableLiveData<Person>()
|
val events: Flow<PagingData<StatusEvent>> = Pager(PagingConfig(20)) {
|
||||||
|
scheduleDao.getEvents(person)
|
||||||
|
}.flow.cachedIn(viewModelScope)
|
||||||
|
|
||||||
val events: LiveData<PagedList<StatusEvent>> = personLiveData.switchMap { person: Person ->
|
@AssistedFactory
|
||||||
scheduleDao.getEvents(person).toLiveData(20)
|
interface Factory {
|
||||||
}
|
fun create(person: Person): PersonInfoViewModel
|
||||||
|
|
||||||
fun setPerson(person: Person) {
|
|
||||||
if (person != personLiveData.value) {
|
|
||||||
personLiveData.value = person
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,16 +1,21 @@
|
||||||
package be.digitalia.fosdem.viewmodels
|
package be.digitalia.fosdem.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.paging.PagedList
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.toLiveData
|
import androidx.paging.Pager
|
||||||
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.cachedIn
|
||||||
import be.digitalia.fosdem.db.ScheduleDao
|
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class PersonsViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
class PersonsViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
||||||
|
|
||||||
val persons: LiveData<PagedList<Person>> = scheduleDao.getPersons().toLiveData(100)
|
val persons: Flow<PagingData<Person>> = Pager(PagingConfig(20)) {
|
||||||
|
scheduleDao.getPersons()
|
||||||
|
}.flow.cachedIn(viewModelScope)
|
||||||
}
|
}
|
|
@ -1,44 +1,49 @@
|
||||||
package be.digitalia.fosdem.viewmodels
|
package be.digitalia.fosdem.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.map
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.switchMap
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.toLiveData
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.cachedIn
|
||||||
import be.digitalia.fosdem.db.ScheduleDao
|
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SearchViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
class SearchViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
||||||
|
|
||||||
private val queryLiveData = MutableLiveData<String>()
|
sealed class QueryState {
|
||||||
|
object Idle : QueryState()
|
||||||
sealed class Result {
|
object TooShort : QueryState()
|
||||||
object QueryTooShort : Result()
|
data class Valid(val query: String) : QueryState()
|
||||||
class Success(val list: PagedList<StatusEvent>) : Result()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val results: LiveData<Result> = queryLiveData.switchMap { query ->
|
private val queryState = MutableStateFlow<QueryState>(QueryState.Idle)
|
||||||
if (query.length < SEARCH_QUERY_MIN_LENGTH) {
|
|
||||||
MutableLiveData(Result.QueryTooShort)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val results: Flow<PagingData<StatusEvent>> = queryState.flatMapLatest { queryState ->
|
||||||
|
if (queryState is QueryState.Valid) {
|
||||||
|
Pager(PagingConfig(20)) {
|
||||||
|
scheduleDao.getSearchResults(queryState.query)
|
||||||
|
}.flow
|
||||||
} else {
|
} else {
|
||||||
scheduleDao.getSearchResults(query)
|
flowOf(PagingData.empty())
|
||||||
.toLiveData(20)
|
|
||||||
.map { pagedList -> Result.Success(pagedList) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}.cachedIn(viewModelScope)
|
||||||
|
|
||||||
fun setQuery(query: String) {
|
fun setQuery(query: String) {
|
||||||
if (query != queryLiveData.value) {
|
queryState.value = if (query.length < SEARCH_QUERY_MIN_LENGTH) QueryState.TooShort
|
||||||
queryLiveData.value = query
|
else QueryState.Valid(query)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val SEARCH_QUERY_MIN_LENGTH = 3
|
const val SEARCH_QUERY_MIN_LENGTH = 3
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,32 +1,29 @@
|
||||||
package be.digitalia.fosdem.viewmodels
|
package be.digitalia.fosdem.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.liveData
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.switchMap
|
|
||||||
import be.digitalia.fosdem.db.ScheduleDao
|
import be.digitalia.fosdem.db.ScheduleDao
|
||||||
import be.digitalia.fosdem.model.Day
|
import be.digitalia.fosdem.model.Day
|
||||||
import be.digitalia.fosdem.model.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 dagger.assisted.Assisted
|
||||||
import javax.inject.Inject
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
|
||||||
@HiltViewModel
|
class TrackScheduleEventViewModel @AssistedInject constructor(
|
||||||
class TrackScheduleEventViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
scheduleDao: ScheduleDao,
|
||||||
|
@Assisted day: Day,
|
||||||
|
@Assisted track: Track
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
private val dayTrackLiveData = MutableLiveData<Pair<Day, Track>>()
|
val scheduleSnapshot: Deferred<List<Event>> = viewModelScope.async {
|
||||||
|
scheduleDao.getEventsWithoutBookmarkStatus(day, track)
|
||||||
val scheduleSnapshot: LiveData<List<Event>> = dayTrackLiveData.switchMap { (day, track) ->
|
|
||||||
liveData {
|
|
||||||
emit(scheduleDao.getEventsSnapshot(day, track))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setDayAndTrack(day: Day, track: Track) {
|
@AssistedFactory
|
||||||
val dayTrack = day to track
|
interface Factory {
|
||||||
if (dayTrack != dayTrackLiveData.value) {
|
fun create(day: Day, track: Track): TrackScheduleEventViewModel
|
||||||
dayTrackLiveData.value = dayTrack
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,56 +1,63 @@
|
||||||
package be.digitalia.fosdem.viewmodels
|
package be.digitalia.fosdem.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.map
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.switchMap
|
|
||||||
import be.digitalia.fosdem.db.ScheduleDao
|
import be.digitalia.fosdem.db.ScheduleDao
|
||||||
import be.digitalia.fosdem.livedata.LiveDataFactory
|
import be.digitalia.fosdem.flow.schedulerFlow
|
||||||
|
import be.digitalia.fosdem.flow.stateFlow
|
||||||
|
import be.digitalia.fosdem.flow.tickerFlow
|
||||||
|
import be.digitalia.fosdem.flow.versionedResourceFlow
|
||||||
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 be.digitalia.fosdem.utils.DateUtils
|
import be.digitalia.fosdem.utils.DateUtils
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
class TrackScheduleListViewModel @AssistedInject constructor(
|
||||||
class TrackScheduleListViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
scheduleDao: ScheduleDao,
|
||||||
|
@Assisted day: Day,
|
||||||
|
@Assisted track: Track
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
private val dayTrackLiveData = MutableLiveData<Pair<Day, Track>>()
|
val schedule: Flow<List<StatusEvent>> = stateFlow(viewModelScope, null) { subscriptionCount ->
|
||||||
|
versionedResourceFlow(scheduleDao.bookmarksVersion, subscriptionCount) {
|
||||||
val schedule: LiveData<List<StatusEvent>> = dayTrackLiveData.switchMap { (day, track) ->
|
|
||||||
scheduleDao.getEvents(day, track)
|
scheduleDao.getEvents(day, track)
|
||||||
}
|
}
|
||||||
|
}.filterNotNull()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The current time during the target day, or null outside of the target day.
|
* @return The current time during the target day, or null outside of the target day.
|
||||||
*/
|
*/
|
||||||
val currentTime: LiveData<Instant?> = dayTrackLiveData
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
.switchMap { (day, _) ->
|
val currentTime: Flow<Instant?> = run {
|
||||||
// Auto refresh during the day passed as argument
|
// Auto refresh during the day passed as argument
|
||||||
val dayStart = day.date.atStartOfDay(DateUtils.conferenceZoneId).toInstant()
|
val dayStart = day.date.atStartOfDay(DateUtils.conferenceZoneId).toInstant()
|
||||||
LiveDataFactory.scheduler(
|
schedulerFlow(
|
||||||
dayStart.toEpochMilli(),
|
dayStart.toEpochMilli(),
|
||||||
(dayStart + Duration.ofDays(1L)).toEpochMilli()
|
(dayStart + Duration.ofDays(1L)).toEpochMilli()
|
||||||
)
|
)
|
||||||
}
|
}.flatMapLatest { isOn ->
|
||||||
.switchMap { isOn ->
|
|
||||||
if (isOn) {
|
if (isOn) {
|
||||||
LiveDataFactory.interval(TIME_REFRESH_PERIOD).map { Instant.now() }
|
tickerFlow(TIME_REFRESH_PERIOD).map { Instant.now() }
|
||||||
} else {
|
} else {
|
||||||
MutableLiveData(null)
|
flowOf(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setDayAndTrack(day: Day, track: Track) {
|
@AssistedFactory
|
||||||
val dayTrack = day to track
|
interface Factory {
|
||||||
if (dayTrack != dayTrackLiveData.value) {
|
fun create(day: Day, track: Track): TrackScheduleListViewModel
|
||||||
dayTrackLiveData.value = dayTrack
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -1,21 +1,26 @@
|
||||||
package be.digitalia.fosdem.viewmodels
|
package be.digitalia.fosdem.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import be.digitalia.fosdem.model.Event
|
import be.digitalia.fosdem.model.Event
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.transform
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewModel used for communication between TrackScheduleActivity and TrackScheduleListFragment
|
* ViewModel used for communication between TrackScheduleActivity and TrackScheduleListFragment
|
||||||
*/
|
*/
|
||||||
class TrackScheduleViewModel : ViewModel() {
|
class TrackScheduleViewModel : ViewModel() {
|
||||||
|
|
||||||
private var _selectedEvent = MutableLiveData<Event?>()
|
private val eventSelection = MutableStateFlow<EventSelection?>(null)
|
||||||
val selectedEvent: LiveData<Event?> = _selectedEvent
|
val selectedEventFlow: Flow<Event?> = eventSelection.transform { selection ->
|
||||||
|
if (selection != null) emit(selection.event)
|
||||||
|
}
|
||||||
|
|
||||||
fun setSelectEvent(event: Event?) {
|
var selectedEvent: Event?
|
||||||
if (event != _selectedEvent.value) {
|
get() = eventSelection.value?.event
|
||||||
_selectedEvent.value = event
|
set(value) {
|
||||||
}
|
eventSelection.value = EventSelection(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class EventSelection(val event: Event?)
|
||||||
}
|
}
|
|
@ -1,27 +1,31 @@
|
||||||
package be.digitalia.fosdem.viewmodels
|
package be.digitalia.fosdem.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.switchMap
|
import androidx.lifecycle.viewModelScope
|
||||||
import be.digitalia.fosdem.db.ScheduleDao
|
import be.digitalia.fosdem.db.ScheduleDao
|
||||||
|
import be.digitalia.fosdem.flow.stateFlow
|
||||||
|
import be.digitalia.fosdem.flow.versionedResourceFlow
|
||||||
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 dagger.assisted.Assisted
|
||||||
import javax.inject.Inject
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
|
||||||
@HiltViewModel
|
class TracksListViewModel @AssistedInject constructor(
|
||||||
class TracksListViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
scheduleDao: ScheduleDao,
|
||||||
|
@Assisted day: Day
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
private val dayLiveData = MutableLiveData<Day>()
|
val tracks: Flow<List<Track>> = stateFlow(viewModelScope, null) { subscriptionCount ->
|
||||||
|
versionedResourceFlow(scheduleDao.version, subscriptionCount) {
|
||||||
val tracks: LiveData<List<Track>> = dayLiveData.switchMap { day: Day ->
|
|
||||||
scheduleDao.getTracks(day)
|
scheduleDao.getTracks(day)
|
||||||
}
|
}
|
||||||
|
}.filterNotNull()
|
||||||
|
|
||||||
fun setDay(day: Day) {
|
@AssistedFactory
|
||||||
if (day != dayLiveData.value) {
|
interface Factory {
|
||||||
dayLiveData.value = day
|
fun create(day: Day): TracksListViewModel
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,19 +1,14 @@
|
||||||
package be.digitalia.fosdem.viewmodels
|
package be.digitalia.fosdem.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.asLiveData
|
|
||||||
import androidx.lifecycle.distinctUntilChanged
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import be.digitalia.fosdem.db.ScheduleDao
|
import be.digitalia.fosdem.db.ScheduleDao
|
||||||
import be.digitalia.fosdem.model.Day
|
import be.digitalia.fosdem.model.Day
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class TracksViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
class TracksViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
||||||
|
|
||||||
val days: LiveData<List<Day>> = scheduleDao.days
|
val days: Flow<List<Day>> = scheduleDao.days
|
||||||
.asLiveData(viewModelScope.coroutineContext)
|
|
||||||
.distinctUntilChanged()
|
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@ import android.widget.ImageButton
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import be.digitalia.fosdem.R
|
import be.digitalia.fosdem.R
|
||||||
import be.digitalia.fosdem.model.BookmarkStatus
|
import be.digitalia.fosdem.model.BookmarkStatus
|
||||||
|
import be.digitalia.fosdem.utils.launchAndRepeatOnLifecycle
|
||||||
import be.digitalia.fosdem.viewmodels.BookmarkStatusViewModel
|
import be.digitalia.fosdem.viewmodels.BookmarkStatusViewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -12,19 +13,22 @@ import be.digitalia.fosdem.viewmodels.BookmarkStatusViewModel
|
||||||
*/
|
*/
|
||||||
fun ImageButton.setupBookmarkStatus(viewModel: BookmarkStatusViewModel, owner: LifecycleOwner) {
|
fun ImageButton.setupBookmarkStatus(viewModel: BookmarkStatusViewModel, owner: LifecycleOwner) {
|
||||||
setOnClickListener { viewModel.toggleBookmarkStatus() }
|
setOnClickListener { viewModel.toggleBookmarkStatus() }
|
||||||
viewModel.bookmarkStatus.observe(owner) { bookmarkStatus: BookmarkStatus? ->
|
var previousBookmarkStatus: BookmarkStatus? = null
|
||||||
|
owner.launchAndRepeatOnLifecycle {
|
||||||
|
viewModel.bookmarkStatus.collect { bookmarkStatus: BookmarkStatus? ->
|
||||||
if (bookmarkStatus == null) {
|
if (bookmarkStatus == null) {
|
||||||
isEnabled = false
|
isEnabled = false
|
||||||
isSelected = false
|
isSelected = false
|
||||||
} else {
|
} else {
|
||||||
val wasEnabled = isEnabled
|
|
||||||
isEnabled = true
|
isEnabled = true
|
||||||
contentDescription = context.getString(if (bookmarkStatus.isBookmarked) R.string.remove_bookmark else R.string.add_bookmark)
|
contentDescription = context.getString(if (bookmarkStatus.isBookmarked) R.string.remove_bookmark else R.string.add_bookmark)
|
||||||
isSelected = bookmarkStatus.isBookmarked
|
isSelected = bookmarkStatus.isBookmarked
|
||||||
// Only animate updates, when the button was already enabled
|
// Only animate when the button was showing the status of the same event
|
||||||
if (!(bookmarkStatus.isUpdate && wasEnabled)) {
|
if (bookmarkStatus.eventId != previousBookmarkStatus?.eventId) {
|
||||||
jumpDrawablesToCurrentState()
|
jumpDrawablesToCurrentState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
previousBookmarkStatus = bookmarkStatus
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue