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.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import com.denizk0461.studip.R
import com.denizk0461.studip.data.StudIPParser
import com.denizk0461.studip.data.getThemedColor
import com.denizk0461.studip.databinding.ActivityFetcherBinding
import com.denizk0461.studip.viewmodel.FetcherViewModel
@@ -72,21 +70,25 @@ class FetcherActivity : Activity() {
) { p0 ->
try {
// Decode HTML and parse it into a list of StudIPEvent.kt
StudIPParser().parse(URLDecoder.decode(p0, "UTF-8")) { events ->
// Delete all previously fetched elements
viewModel.nukeEvents()
// Insert the list into the database
viewModel.insertEvents(events)
// Notify the user that the fetch was successful
Toast.makeText(
this, R.string.toast_fetch_finished, Toast.LENGTH_SHORT
).show()
// Close the activity
finish()
}
// TODO this exception handling and finish() is broken
viewModel.parse(URLDecoder.decode(p0, "UTF-8"))
// StudIPParser(application).parse(URLDecoder.decode(p0, "UTF-8")) { events ->
//
// // Delete all previously fetched elements
//// viewModel.nukeEvents()
//// viewModel.replaceEvents(events)
//
// // Insert the list into the database
//// viewModel.insertEvents(events)
//
// // Notify the user that the fetch was successful
// Toast.makeText(
// this, R.string.toast_fetch_finished, Toast.LENGTH_SHORT
// ).show()
//
// // Close the activity
// finish()
// }
} catch (e: IOException) {
// Let the user know that an error occurred
@@ -1,12 +1,11 @@
package com.denizk0461.studip.activity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Lifecycle
import com.denizk0461.studip.R
import com.denizk0461.studip.data.Dependencies
import com.denizk0461.studip.databinding.ActivityMainBinding
import com.denizk0461.studip.db.AppRepository
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.
*/
class MainActivity : AppCompatActivity() {
class MainActivity : FragmentActivity() {
// View binding
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
* data.
*/
Dependencies.repo = AppRepository(application)
// Dependencies.repo = AppRepository(application)
// Inflate view binding and bind to this activity
binding = ActivityMainBinding.inflate(layoutInflater)
@@ -44,7 +43,8 @@ class MainActivity : AppCompatActivity() {
// Open the fragment that the user specified to open on app start
if (
Dependencies.repo.getBooleanPreference(SettingsPreferences.LAUNCH_CANTEEN_ON_START)
AppRepository.getRepositoryInstance(application)
.getBooleanPreference(SettingsPreferences.LAUNCH_CANTEEN_ON_START)
) {
// Open canteen fragment
binding.contentMain.navView.selectedItemId = R.id.food
@@ -27,7 +27,7 @@ class StudIPEventItemAdapter(
) : 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()
@@ -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
import android.app.Application
import com.denizk0461.studip.db.AppRepository
import com.denizk0461.studip.model.StudIPEvent
import org.jsoup.Jsoup
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.
*/
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.
@@ -84,7 +88,7 @@ class StudIPParser {
// Construct the newly scraped Stud.IP event
val event = StudIPEvent(
id = id,
eventId = id,
title = parsedTitle,
lecturer = parsedLecturers,
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
insert(newEvents)
repo.insertEvents(newEvents)
}
}
@@ -1,5 +1,7 @@
package com.denizk0461.studip.data
import android.app.Application
import com.denizk0461.studip.db.AppRepository
import com.denizk0461.studip.model.*
import org.jsoup.Jsoup
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
* Studierendenwerk Bremen.
*/
class StwParser {
class StwParser(application: Application) {
// Unique primary key value for the date elements
private var dateId = 0
@@ -26,6 +28,11 @@ class StwParser {
// Determine whether there are offers for a given date
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.
*
@@ -59,13 +66,13 @@ class StwParser {
}
// Delete all previous entries and start afresh
Dependencies.repo.nukeOffers()
repo.nukeOffers()
// Fetch offers from the chosen canteen
val (dates, canteens, categories, items) = parseFromPage(link)
// Save everything all at once into the database
with (Dependencies.repo) {
with (repo) {
insertDates(dates)
insertCanteens(canteens)
insertCategories(categories)
@@ -18,7 +18,7 @@ interface AppDAO {
*
* @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>>
/**
@@ -26,7 +26,7 @@ interface AppDAO {
*
* @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>>
/**
@@ -62,7 +62,7 @@ interface AppDAO {
/**
* Deletes all Stud.IP events from the database.
*/
@Query("DELETE FROM studipevents")
@Query("DELETE FROM studip_events")
fun nukeEvents()
/* --- canteen offers --- */
@@ -17,7 +17,7 @@ import com.denizk0461.studip.model.*
OfferCategory::class,
OfferItem::class,
],
version = 16,
version = 18,
)
abstract class AppDatabase : RoomDatabase() {
@@ -25,6 +25,27 @@ class AppRepository(app: Application) {
// Blank dietary preference regular expression
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.
*
@@ -66,7 +66,7 @@ class EventPageFragment(
// Set up LiveData observer to refresh the view on update
viewModel.getEventsForDay(currentDay).observe(viewLifecycleOwner) { events ->
// eventAdapter.setNewItems(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.
*
* @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 lecturer lecturer(s) organising the event
* @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 colour user-defined colour the event will be shown in - UNIMPLEMENTED
*/
@Entity(tableName = "studipevents")
@Entity(tableName = "studip_events")
data class StudIPEvent(
@PrimaryKey val id: Int,
@PrimaryKey val eventId: Int,
val title: String,
val lecturer: String,
val room: String,
@@ -45,7 +45,7 @@ data class StudIPEvent(
other as StudIPEvent
if (id != other.id) return false
if (eventId != other.eventId) return false
if (title != other.title) return false
if (lecturer != other.lecturer) return false
if (room != other.room) return false
@@ -59,7 +59,7 @@ data class StudIPEvent(
}
override fun hashCode(): Int {
var result = id
var result = eventId
result = 31 * result + title.hashCode()
result = 31 * result + lecturer.hashCode()
result = 31 * result + room.hashCode()
@@ -133,7 +133,7 @@ class ScheduleUpdateSheet(
onUpdate(
// construct new StudIPEvent from the data the user may have edited
StudIPEvent(
id = event.id,
eventId = event.eventId,
title = binding.editTextTitle.text.toString(),
lecturer = binding.editTextLecturers.text.toString(),
room = binding.editTextRoom.text.toString(),
@@ -3,7 +3,6 @@ package com.denizk0461.studip.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.denizk0461.studip.data.Dependencies
import com.denizk0461.studip.db.AppRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -21,7 +20,7 @@ open class AppViewModel(app: Application) : AndroidViewModel(app) {
/**
* 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
@@ -13,7 +13,7 @@ import com.denizk0461.studip.model.*
class CanteenViewModel(app: Application) : AppViewModel(app) {
// 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.
@@ -1,7 +1,7 @@
package com.denizk0461.studip.viewmodel
import android.app.Application
import com.denizk0461.studip.model.StudIPEvent
import com.denizk0461.studip.data.StudIPParser
/**
* View model for [com.denizk0461.studip.activity.FetcherActivity]
@@ -10,15 +10,7 @@ import com.denizk0461.studip.model.StudIPEvent
*/
class FetcherViewModel(app: Application) : AppViewModel(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) }}
private val parser: StudIPParser = StudIPParser(app)
/**
* Delete all Stud.IP events from the database asynchronously.
*/
fun nukeEvents() { doAsync { repo.nukeEvents() } }
fun parse(html: String) { doAsync { parser.parse(html, {}) }}
}