Possibly fixed-for-good a bug that caused events to be fetched but not stored properly in the database; I believe this was caused by FetcherActivity.kt sequentially executing nukeEvents() and insertEvents(), but doing both asynchronously, and individually, which could mean that insertEvents() finishes before nukeEvents() and therefore nukeEvents() would delete the old events as well as the new ones. This doesn't seem entirely logical to me, as no ConflictStrategy has been configured, and inserting new events before the old ones have been cleared (since they both start their primary keys at 0) would have crashed the app.

This commit is contained in:
denizk0461
2023-04-28 15:14:50 +02:00
parent aa5d32e64a
commit 509a0434c5
15 changed files with 82 additions and 69 deletions
@@ -6,10 +6,8 @@ import android.os.Bundle
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.denizk0461.studip.R import com.denizk0461.studip.R
import com.denizk0461.studip.data.StudIPParser
import com.denizk0461.studip.data.getThemedColor import com.denizk0461.studip.data.getThemedColor
import com.denizk0461.studip.databinding.ActivityFetcherBinding import com.denizk0461.studip.databinding.ActivityFetcherBinding
import com.denizk0461.studip.viewmodel.FetcherViewModel import com.denizk0461.studip.viewmodel.FetcherViewModel
@@ -72,21 +70,25 @@ class FetcherActivity : Activity() {
) { p0 -> ) { p0 ->
try { try {
// Decode HTML and parse it into a list of StudIPEvent.kt // Decode HTML and parse it into a list of StudIPEvent.kt
StudIPParser().parse(URLDecoder.decode(p0, "UTF-8")) { events -> // TODO this exception handling and finish() is broken
// Delete all previously fetched elements viewModel.parse(URLDecoder.decode(p0, "UTF-8"))
viewModel.nukeEvents() // StudIPParser(application).parse(URLDecoder.decode(p0, "UTF-8")) { events ->
//
// Insert the list into the database // // Delete all previously fetched elements
viewModel.insertEvents(events) //// viewModel.nukeEvents()
//// viewModel.replaceEvents(events)
// Notify the user that the fetch was successful //
Toast.makeText( // // Insert the list into the database
this, R.string.toast_fetch_finished, Toast.LENGTH_SHORT //// viewModel.insertEvents(events)
).show() //
// // Notify the user that the fetch was successful
// Close the activity // Toast.makeText(
finish() // this, R.string.toast_fetch_finished, Toast.LENGTH_SHORT
} // ).show()
//
// // Close the activity
// finish()
// }
} catch (e: IOException) { } catch (e: IOException) {
// Let the user know that an error occurred // Let the user know that an error occurred
@@ -1,12 +1,11 @@
package com.denizk0461.studip.activity package com.denizk0461.studip.activity
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import com.denizk0461.studip.R import com.denizk0461.studip.R
import com.denizk0461.studip.data.Dependencies
import com.denizk0461.studip.databinding.ActivityMainBinding import com.denizk0461.studip.databinding.ActivityMainBinding
import com.denizk0461.studip.db.AppRepository import com.denizk0461.studip.db.AppRepository
import com.denizk0461.studip.fragment.EventFragment import com.denizk0461.studip.fragment.EventFragment
@@ -17,7 +16,7 @@ import com.denizk0461.studip.model.SettingsPreferences
/** /**
* Main activity that handles all common fragments. This is opened on app launch. * Main activity that handles all common fragments. This is opened on app launch.
*/ */
class MainActivity : AppCompatActivity() { class MainActivity : FragmentActivity() {
// View binding // View binding
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
@@ -36,7 +35,7 @@ class MainActivity : AppCompatActivity() {
* Instantiate repository object that is accessed by the fragments' view models to retrieve * Instantiate repository object that is accessed by the fragments' view models to retrieve
* data. * data.
*/ */
Dependencies.repo = AppRepository(application) // Dependencies.repo = AppRepository(application)
// Inflate view binding and bind to this activity // Inflate view binding and bind to this activity
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
@@ -44,7 +43,8 @@ class MainActivity : AppCompatActivity() {
// Open the fragment that the user specified to open on app start // Open the fragment that the user specified to open on app start
if ( if (
Dependencies.repo.getBooleanPreference(SettingsPreferences.LAUNCH_CANTEEN_ON_START) AppRepository.getRepositoryInstance(application)
.getBooleanPreference(SettingsPreferences.LAUNCH_CANTEEN_ON_START)
) { ) {
// Open canteen fragment // Open canteen fragment
binding.contentMain.navView.selectedItemId = R.id.food binding.contentMain.navView.selectedItemId = R.id.food
@@ -27,7 +27,7 @@ class StudIPEventItemAdapter(
) : RecyclerView.Adapter<StudIPEventItemAdapter.EventViewHolder>() { ) : RecyclerView.Adapter<StudIPEventItemAdapter.EventViewHolder>() {
/** /**
* List of all events (still not filtered by day at this point) * List of all events.
*/ */
private var events: MutableList<StudIPEvent> = mutableListOf() private var events: MutableList<StudIPEvent> = mutableListOf()
@@ -1,15 +0,0 @@
package com.denizk0461.studip.data
import com.denizk0461.studip.db.AppRepository
/**
* Static object that holds a reference to the app's repository to provide abstracted access to the
* app database.
*/
object Dependencies {
/**
* Reference to the app's repository. Since it instantiated once by the main activity, it can be
* accessed statically by all other classes. Used for database transactions.
*/
lateinit var repo: AppRepository
}
@@ -1,5 +1,7 @@
package com.denizk0461.studip.data package com.denizk0461.studip.data
import android.app.Application
import com.denizk0461.studip.db.AppRepository
import com.denizk0461.studip.model.StudIPEvent import com.denizk0461.studip.model.StudIPEvent
import org.jsoup.Jsoup import org.jsoup.Jsoup
import java.io.IOException import java.io.IOException
@@ -7,7 +9,9 @@ import java.io.IOException
/** /**
* Parser class used for fetching and collecting scheduled events from a Stud.IP timetable. * Parser class used for fetching and collecting scheduled events from a Stud.IP timetable.
*/ */
class StudIPParser { class StudIPParser(application: Application) {
private val repo: AppRepository = AppRepository.getRepositoryInstance(application)
/** /**
* Parse a given HTML string. HTML must be of a Stud.IP timetable. * Parse a given HTML string. HTML must be of a Stud.IP timetable.
@@ -84,7 +88,7 @@ class StudIPParser {
// Construct the newly scraped Stud.IP event // Construct the newly scraped Stud.IP event
val event = StudIPEvent( val event = StudIPEvent(
id = id, eventId = id,
title = parsedTitle, title = parsedTitle,
lecturer = parsedLecturers, lecturer = parsedLecturers,
room = entryInfo[1], room = entryInfo[1],
@@ -102,7 +106,10 @@ class StudIPParser {
} }
} }
// Delete all previously saved events
repo.nukeEvents()
// After fetching has finished, save the list of new items into persistent storage // After fetching has finished, save the list of new items into persistent storage
insert(newEvents) repo.insertEvents(newEvents)
} }
} }
@@ -1,5 +1,7 @@
package com.denizk0461.studip.data package com.denizk0461.studip.data
import android.app.Application
import com.denizk0461.studip.db.AppRepository
import com.denizk0461.studip.model.* import com.denizk0461.studip.model.*
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
@@ -9,7 +11,7 @@ import org.jsoup.select.Elements
* Parser class used for fetching and collecting canteen offers from the website of the * Parser class used for fetching and collecting canteen offers from the website of the
* Studierendenwerk Bremen. * Studierendenwerk Bremen.
*/ */
class StwParser { class StwParser(application: Application) {
// Unique primary key value for the date elements // Unique primary key value for the date elements
private var dateId = 0 private var dateId = 0
@@ -26,6 +28,11 @@ class StwParser {
// Determine whether there are offers for a given date // Determine whether there are offers for a given date
private var dateHasItems = false private var dateHasItems = false
/**
* Static instance of the app's repository.
*/
private val repo: AppRepository = AppRepository.getRepositoryInstance(application)
/** /**
* Parses through a list of canteen plans and saves them to persistent storage. * Parses through a list of canteen plans and saves them to persistent storage.
* *
@@ -59,13 +66,13 @@ class StwParser {
} }
// Delete all previous entries and start afresh // Delete all previous entries and start afresh
Dependencies.repo.nukeOffers() repo.nukeOffers()
// Fetch offers from the chosen canteen // Fetch offers from the chosen canteen
val (dates, canteens, categories, items) = parseFromPage(link) val (dates, canteens, categories, items) = parseFromPage(link)
// Save everything all at once into the database // Save everything all at once into the database
with (Dependencies.repo) { with (repo) {
insertDates(dates) insertDates(dates)
insertCanteens(canteens) insertCanteens(canteens)
insertCategories(categories) insertCategories(categories)
@@ -18,7 +18,7 @@ interface AppDAO {
* *
* @return all Stud.IP events exposed through a LiveData object * @return all Stud.IP events exposed through a LiveData object
*/ */
@get:Query("SELECT * FROM studipevents ORDER BY timeslotId, id") @get:Query("SELECT * FROM studip_events ORDER BY timeslotId, eventId")
val allEvents: LiveData<List<StudIPEvent>> val allEvents: LiveData<List<StudIPEvent>>
/** /**
@@ -26,7 +26,7 @@ interface AppDAO {
* *
* @return Stud.IP events for a certain day exposed through a LiveData object * @return Stud.IP events for a certain day exposed through a LiveData object
*/ */
@Query("SELECT * FROM studipevents WHERE day = :day ORDER BY timeslotId, id") @Query("SELECT * FROM studip_events WHERE day = :day ORDER BY timeslotId, eventId")
fun getEventsForDay(day: Int): LiveData<List<StudIPEvent>> fun getEventsForDay(day: Int): LiveData<List<StudIPEvent>>
/** /**
@@ -62,7 +62,7 @@ interface AppDAO {
/** /**
* Deletes all Stud.IP events from the database. * Deletes all Stud.IP events from the database.
*/ */
@Query("DELETE FROM studipevents") @Query("DELETE FROM studip_events")
fun nukeEvents() fun nukeEvents()
/* --- canteen offers --- */ /* --- canteen offers --- */
@@ -17,7 +17,7 @@ import com.denizk0461.studip.model.*
OfferCategory::class, OfferCategory::class,
OfferItem::class, OfferItem::class,
], ],
version = 16, version = 18,
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@@ -25,6 +25,27 @@ class AppRepository(app: Application) {
// Blank dietary preference regular expression // Blank dietary preference regular expression
private val blankDietaryPrefs: String = DietaryPreferences.C_FALSE.toString().repeat(10) private val blankDietaryPrefs: String = DietaryPreferences.C_FALSE.toString().repeat(10)
companion object {
/**
* Reference to the app's repository. It is only instantiated once because it can only be
* accessed by [getRepositoryInstance].
*/
private lateinit var repo: AppRepository
/**
* Get a static instance of the repository. If none has been instantiated, one will be created
* and saved into [repo].
*
* @return static instance of the repository
*/
fun getRepositoryInstance(app: Application): AppRepository {
if (!::repo.isInitialized) {
repo = AppRepository(app)
}
return repo
}
}
/** /**
* Retrieves Stud.IP events for a specific day, ordered by their timeslots, then their IDs. * Retrieves Stud.IP events for a specific day, ordered by their timeslots, then their IDs.
* *
@@ -66,7 +66,7 @@ class EventPageFragment(
// Set up LiveData observer to refresh the view on update // Set up LiveData observer to refresh the view on update
viewModel.getEventsForDay(currentDay).observe(viewLifecycleOwner) { events -> viewModel.getEventsForDay(currentDay).observe(viewLifecycleOwner) { events ->
// eventAdapter.setNewItems(events)
eventAdapter.setNewData(events) eventAdapter.setNewData(events)
} }
} }
@@ -6,7 +6,7 @@ import androidx.room.PrimaryKey
/** /**
* Entity for storing entries of the user's Stud.IP schedule. Registered in the app database. * Entity for storing entries of the user's Stud.IP schedule. Registered in the app database.
* *
* @param id primary key that uniquely identifies the entry * @param eventId primary key that uniquely identifies the entry
* @param title title of the event * @param title title of the event
* @param lecturer lecturer(s) organising the event * @param lecturer lecturer(s) organising the event
* @param room room the event takes place in * @param room room the event takes place in
@@ -16,9 +16,9 @@ import androidx.room.PrimaryKey
* @param timeslotId minute the event starts at - used for ordering * @param timeslotId minute the event starts at - used for ordering
* @param colour user-defined colour the event will be shown in - UNIMPLEMENTED * @param colour user-defined colour the event will be shown in - UNIMPLEMENTED
*/ */
@Entity(tableName = "studipevents") @Entity(tableName = "studip_events")
data class StudIPEvent( data class StudIPEvent(
@PrimaryKey val id: Int, @PrimaryKey val eventId: Int,
val title: String, val title: String,
val lecturer: String, val lecturer: String,
val room: String, val room: String,
@@ -45,7 +45,7 @@ data class StudIPEvent(
other as StudIPEvent other as StudIPEvent
if (id != other.id) return false if (eventId != other.eventId) return false
if (title != other.title) return false if (title != other.title) return false
if (lecturer != other.lecturer) return false if (lecturer != other.lecturer) return false
if (room != other.room) return false if (room != other.room) return false
@@ -59,7 +59,7 @@ data class StudIPEvent(
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = id var result = eventId
result = 31 * result + title.hashCode() result = 31 * result + title.hashCode()
result = 31 * result + lecturer.hashCode() result = 31 * result + lecturer.hashCode()
result = 31 * result + room.hashCode() result = 31 * result + room.hashCode()
@@ -133,7 +133,7 @@ class ScheduleUpdateSheet(
onUpdate( onUpdate(
// construct new StudIPEvent from the data the user may have edited // construct new StudIPEvent from the data the user may have edited
StudIPEvent( StudIPEvent(
id = event.id, eventId = event.eventId,
title = binding.editTextTitle.text.toString(), title = binding.editTextTitle.text.toString(),
lecturer = binding.editTextLecturers.text.toString(), lecturer = binding.editTextLecturers.text.toString(),
room = binding.editTextRoom.text.toString(), room = binding.editTextRoom.text.toString(),
@@ -3,7 +3,6 @@ package com.denizk0461.studip.viewmodel
import android.app.Application import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.denizk0461.studip.data.Dependencies
import com.denizk0461.studip.db.AppRepository import com.denizk0461.studip.db.AppRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -21,7 +20,7 @@ open class AppViewModel(app: Application) : AndroidViewModel(app) {
/** /**
* Reference to the app's repository for database transactions. * Reference to the app's repository for database transactions.
*/ */
protected val repo: AppRepository = Dependencies.repo protected val repo: AppRepository = AppRepository.getRepositoryInstance(app)
/** /**
* Execute a function asynchronously on the I/O thread. Be careful not to execute UI commands * Execute a function asynchronously on the I/O thread. Be careful not to execute UI commands
@@ -13,7 +13,7 @@ import com.denizk0461.studip.model.*
class CanteenViewModel(app: Application) : AppViewModel(app) { class CanteenViewModel(app: Application) : AppViewModel(app) {
// Instantiate a parser, should the user want to refresh the canteen offers // Instantiate a parser, should the user want to refresh the canteen offers
private val parser = StwParser() private val parser = StwParser(app)
/** /**
* Retrieves all Stud.IP events. * Retrieves all Stud.IP events.
@@ -1,7 +1,7 @@
package com.denizk0461.studip.viewmodel package com.denizk0461.studip.viewmodel
import android.app.Application import android.app.Application
import com.denizk0461.studip.model.StudIPEvent import com.denizk0461.studip.data.StudIPParser
/** /**
* View model for [com.denizk0461.studip.activity.FetcherActivity] * View model for [com.denizk0461.studip.activity.FetcherActivity]
@@ -10,15 +10,7 @@ import com.denizk0461.studip.model.StudIPEvent
*/ */
class FetcherViewModel(app: Application) : AppViewModel(app) { class FetcherViewModel(app: Application) : AppViewModel(app) {
/** private val parser: StudIPParser = StudIPParser(app)
* Save a list of Stud.IP events to persistent storage asynchronously.
*
* @param events list of events to be saved
*/
fun insertEvents(events: List<StudIPEvent>) { doAsync { repo.insertEvents(events) }}
/** fun parse(html: String) { doAsync { parser.parse(html, {}) }}
* Delete all Stud.IP events from the database asynchronously.
*/
fun nukeEvents() { doAsync { repo.nukeEvents() } }
} }