Implemented snackbar telling the user that an error has occurred when fetching their schedule through FetcherActivity.kt. Also made some slight additions to the documentation

This commit is contained in:
denizk0461
2023-04-25 12:02:25 +02:00
parent 96862497e4
commit 840c8f1db7
7 changed files with 89 additions and 26 deletions
@@ -3,6 +3,7 @@ package com.denizk0461.studip.activity
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
@@ -12,6 +13,8 @@ import com.denizk0461.studip.R
import com.denizk0461.studip.data.StudIPParser import com.denizk0461.studip.data.StudIPParser
import com.denizk0461.studip.databinding.ActivityFetcherBinding import com.denizk0461.studip.databinding.ActivityFetcherBinding
import com.denizk0461.studip.viewmodel.FetcherViewModel import com.denizk0461.studip.viewmodel.FetcherViewModel
import com.google.android.material.snackbar.Snackbar
import java.io.IOException
import java.net.URLDecoder import java.net.URLDecoder
/** /**
@@ -67,18 +70,43 @@ class FetcherActivity : Activity() {
binding.webview.evaluateJavascript( binding.webview.evaluateJavascript(
"(function(){return encodeURI(document.getElementsByTagName('html')[0].innerHTML)})();" "(function(){return encodeURI(document.getElementsByTagName('html')[0].innerHTML)})();"
) { p0 -> ) { p0 ->
// Delete all previously fetched elements try {
viewModel.nukeEvents() // 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 ->
StudIPParser().parse(URLDecoder.decode(p0, "UTF-8")) { events -> // Delete all previously fetched elements
// Insert the list into the database viewModel.nukeEvents()
viewModel.insertEvents(events) // Insert the list into the database
// Notify the user that the fetch was successful. TODO: notify on error viewModel.insertEvents(events)
Toast.makeText( // Notify the user that the fetch was successful
this, R.string.toast_fetch_finished, Toast.LENGTH_SHORT Toast.makeText(
).show() this, R.string.toast_fetch_finished, Toast.LENGTH_SHORT
// Close the activity ).show()
finish() // Close the activity
finish()
}
} catch (e: IOException) {
// Set up error colours
val colorErrorContainer = TypedValue()
val colorOnErrorContainer = TypedValue()
// Resolve themed attributes to get the right values for day and night modes
theme.apply {
resolveAttribute(R.attr.colorErrorContainer, colorErrorContainer, true)
resolveAttribute(R.attr.colorOnErrorContainer, colorOnErrorContainer, true)
}
// Let the user know that an error occurred
Snackbar
.make(
binding.rootView,
getString(R.string.fetch_error_snack),
Snackbar.LENGTH_SHORT
)
// Set colours to signify an error
.setBackgroundTint(colorErrorContainer.data)
.setTextColor(colorOnErrorContainer.data)
.show()
} }
} }
} }
@@ -2,6 +2,7 @@ package com.denizk0461.studip.data
import com.denizk0461.studip.model.StudIPEvent import com.denizk0461.studip.model.StudIPEvent
import org.jsoup.Jsoup import org.jsoup.Jsoup
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.
@@ -11,9 +12,11 @@ class StudIPParser {
/** /**
* 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.
* *
* @param html website content of the Stud.IP timetable * @param html website content of the Stud.IP timetable
* @param insert action call to save the contents to persistent storage * @param insert action call to save the contents to persistent storage
* @throws IOException when an error in fetching or the database transaction occurs
*/ */
@kotlin.jvm.Throws(IOException::class)
fun parse(html: String, insert: (events: List<StudIPEvent>) -> Unit) { fun parse(html: String, insert: (events: List<StudIPEvent>) -> Unit) {
// Primary key value to uniquely identify entries in the database // Primary key value to uniquely identify entries in the database
var id = 0 var id = 0
@@ -2,7 +2,6 @@ package com.denizk0461.studip.sheet
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.TimePicker
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.denizk0461.studip.R import com.denizk0461.studip.R
import com.denizk0461.studip.data.parseToMinutes import com.denizk0461.studip.data.parseToMinutes
@@ -11,6 +10,13 @@ import com.denizk0461.studip.databinding.SheetScheduleUpdateBinding
import com.denizk0461.studip.dialog.TimePickerFragment import com.denizk0461.studip.dialog.TimePickerFragment
import com.denizk0461.studip.model.StudIPEvent import com.denizk0461.studip.model.StudIPEvent
/**
* Update sheet to allow the user to edit attributes for a specific schedule event.
*
* @param event event to be updated
* @param onUpdate action to execute to update the event
* @param onDelete action to execute to delete the event
*/
class ScheduleUpdateSheet( class ScheduleUpdateSheet(
private val event: StudIPEvent, private val event: StudIPEvent,
private val onUpdate: (event: StudIPEvent) -> Unit, private val onUpdate: (event: StudIPEvent) -> Unit,
@@ -28,24 +34,37 @@ class ScheduleUpdateSheet(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// Set editable text fields
binding.editTextTitle.setText(event.title) binding.editTextTitle.setText(event.title)
binding.editTextLecturers.setText(event.lecturer) binding.editTextLecturers.setText(event.lecturer)
binding.editTextRoom.setText(event.room) binding.editTextRoom.setText(event.room)
// Prepare timestamp buttons
binding.buttonTimeStart.text = timeslotStart binding.buttonTimeStart.text = timeslotStart
binding.buttonTimeStart.setOnClickListener { binding.buttonTimeStart.setOnClickListener {
TimePickerFragment(binding.buttonTimeStart.text.toString(), this, true).show((context as FragmentActivity).supportFragmentManager, "timePicker") TimePickerFragment(
binding.buttonTimeStart.text.toString(),
this,
true,
).show((context as FragmentActivity).supportFragmentManager, "timePicker")
} }
binding.buttonTimeEnd.text = timeslotEnd binding.buttonTimeEnd.text = timeslotEnd
binding.buttonTimeEnd.setOnClickListener { binding.buttonTimeEnd.setOnClickListener {
TimePickerFragment(binding.buttonTimeEnd.text.toString(), this, false).show((context as FragmentActivity).supportFragmentManager, "timePicker") TimePickerFragment(
binding.buttonTimeEnd.text.toString(),
this,
false,
).show((context as FragmentActivity).supportFragmentManager, "timePicker")
} }
// Prepare delete button
binding.buttonDelete.setOnClickListener { binding.buttonDelete.setOnClickListener {
onDelete(event) onDelete(event)
// Dismiss the sheet upon deletion
dismiss() dismiss()
} }
// Prepare update/save button
binding.buttonSave.setOnClickListener { binding.buttonSave.setOnClickListener {
onUpdate( onUpdate(
StudIPEvent( StudIPEvent(
@@ -60,22 +79,28 @@ class ScheduleUpdateSheet(
colour = colour, colour = colour,
) )
) )
// Dismiss the sheet upon update
dismiss() dismiss()
} }
} }
/** /**
* Checks and stores a new timestamp after the user picks one through [TimePickerFragment].
* *
* @param hours newly set hour
* @param minutes newly set minute
* @param isEventStart whether this timestamp is for the start of the course
*/ */
override fun onTimeSet(hours: Int, minutes: Int, isEventStart: Boolean) { override fun onTimeSet(hours: Int, minutes: Int, isEventStart: Boolean) {
// Parse the separate ints into a timestamp string
val newTimestamp = "$hours:$minutes" val newTimestamp = "$hours:$minutes"
// If start > end, show an error
if ((isEventStart && newTimestamp.parseToMinutes() > timeslotEnd.parseToMinutes()) || if ((isEventStart && newTimestamp.parseToMinutes() > timeslotEnd.parseToMinutes()) ||
(!isEventStart && newTimestamp.parseToMinutes() < timeslotStart.parseToMinutes())) { (!isEventStart && newTimestamp.parseToMinutes() < timeslotStart.parseToMinutes())) {
// tell user that the timestamp must be set earlier/later // tell user that the timestamp must be set earlier/later
} else { } else {
// If the new timestamp is valid, save it
if (isEventStart) { if (isEventStart) {
timeslotStart = newTimestamp timeslotStart = newTimestamp
binding.buttonTimeStart.text = newTimestamp binding.buttonTimeStart.text = newTimestamp
+4 -4
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@@ -17,10 +18,9 @@
android:id="@+id/fab" android:id="@+id/fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" android:layout_gravity="bottom|end"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="@dimen/fab_margin" android:layout_marginEnd="@dimen/fab_margin"
android:layout_marginBottom="@dimen/fab_margin" android:layout_marginBottom="@dimen/fab_margin"
app:srcCompat="@drawable/check" /> app:srcCompat="@drawable/check" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
+4 -2
View File
@@ -9,6 +9,8 @@
<string name="saturday">Samstag</string> <string name="saturday">Samstag</string>
<string name="sunday">Sonntag</string> <string name="sunday">Sonntag</string>
<string name="fetch_error_snack">Ein Fehler ist aufgetreten!</string>
<string name="save">Speichern</string> <string name="save">Speichern</string>
<string name="delete">Löschen</string> <string name="delete">Löschen</string>
<string name="sheet_schedule_update_header">Veranstaltung bearbeiten</string> <string name="sheet_schedule_update_header">Veranstaltung bearbeiten</string>
@@ -118,9 +120,9 @@
<string name="settings_data_title">Was passiert mit meinen Daten?</string> <string name="settings_data_title">Was passiert mit meinen Daten?</string>
<string name="settings_data_subtitle">Stiehlst du meinen Stud.IP-Login?</string> <string name="settings_data_subtitle">Stiehlst du meinen Stud.IP-Login?</string>
<string name="settings_data_sheet_header">TL;DR: nein, aber Google könnte mir Bescheid geben, falls die App mal abstürzt.</string> <string name="settings_data_sheet_header">TL;DR: nein, aber Google könnte mir Bescheid geben, falls die App mal abstürzt.</string>
<string name="settings_data_sheet_content">Die App funktioniert standalone, was bedeutet, dass sie sich niemals mit einem Server verbindet, der mir gehört. Sie ist ein Web-Scraper, also ein Programm, welches eine Internetseite öffnet und dann all die Informationen dieser Webseite herunterlädt und analysiert. In diesem Fall sind es die Webseiten der Mensen und Cafeterien des Studierendenwerks Bremen (stw-bremen.de) sowie die Stud.IP-Seite (elearning.uni-bremen.de).\n\nDies funktioniert relativ einfach für die Essenspläne, da diese Pläne öffentlich zugänglich sind. Um jedoch Deinen persönlichen Stud.IP-Stundenplan herunterzuladen, braucht die App Zugriff auf diese Webseite während Dein Account eingeloggt ist. Dies erfolgt, indem die App Dich mit deinem Account einloggen lässt - dies geschieht, wenn Du den Knopf zum Aktualisieren des Plans drückst und die App Dir die Login-Seite zeigt - und Du dann den Knopf klickst, um den Inhalt, den HTML-Quellcode, herunterzuladen. Der HTML-Quellcode beinhält Informationen darüber, was auf einer Webseite angezeigt wird und wie es aussehen soll, aber er kann niemals persönliche Details beinhalten, wie zum Beispiel Passwörter oder andere Daten, die auf Deinem Gerät vorhanden sind.\n\nDer Quellcode wird dann lokal, also auf Deinem Gerät, zu den Karten verarbeitet, die Du dann in der App sehen kannst, ohne, dass jegliche Daten irgendwohin geschickt werden müssten.\n\nEinige Daten könnten jedoch dennoch nach außen geschickt werden. Diese App nutzt \"Crashlytics\" von Google. Dies ist ein Tool, welches Absturzreporte an Google-Server schickt, wenn in der App etwas schief läuft. Diese werden genutzt, um Probleme in der App zu lösen.\n\nDies ist jedoch gemäß europäischer Datenschutz-Grundverordnung Opt-In, was bedeutet, dass Du zustimmen musst, bevor Daten nach außen geschickt werden. Du wurdest danach gefragt, als Du die App zum ersten Mal gestartet hast, und du kannst jederzeit deine Meinung ändern, indem Du den Schalter hierfür in den Einstellungen umstellst. Solltest Du dich entscheiden, keine Daten verschicken zu lassen, wird auch nichts mit Google (und mir) geteilt.\n\nSolltest du dich versichern wollen, dass wirklich keine Daten an irgendwelche Server verschickt wird (abgesehen von den Absturzreports an Google), kannst Du jederzeit das Projekt erkunden, da es Open-Source und auf meinen GitHub-Profil öffentlich zugänglich ist:\nhttps://github.com/denizk0461/studip-timetable/</string> <string name="settings_data_sheet_content">Die App funktioniert standalone, was bedeutet, dass sie sich niemals mit einem Server verbindet, der mir gehört. Sie ist ein Web-Scraper, also ein Programm, welches eine Internetseite öffnet und dann all die Informationen dieser Webseite herunterlädt und analysiert. In diesem Fall sind es die Webseiten der Mensen und Cafeterien des Studierendenwerks Bremen (stw-bremen.de) sowie die Stud.IP-Seite (elearning.uni-bremen.de).\n\nDies funktioniert relativ einfach für die Essenspläne, da diese Pläne öffentlich zugänglich sind. Um jedoch Deinen persönlichen Stud.IP-Stundenplan herunterzuladen, braucht die App Zugriff auf diese Webseite während Dein Account eingeloggt ist. Dies erfolgt, indem die App Dich mit deinem Account einloggen lässt - dies geschieht, wenn Du den Knopf zum Aktualisieren des Plans drückst und die App Dir die Login-Seite zeigt - und Du dann den Knopf klickst, um den Inhalt, den HTML-Quellcode, herunterzuladen. Der HTML-Quellcode beinhält Informationen darüber, was auf einer Webseite angezeigt wird und wie es aussehen soll, aber er kann niemals persönliche Details beinhalten, wie zum Beispiel Passwörter oder andere Daten, die auf Deinem Gerät vorhanden sind.\n\nDer Quellcode wird dann lokal, also auf Deinem Gerät, zu den Karten verarbeitet, die Du dann in der App sehen kannst, ohne, dass jegliche Daten irgendwohin geschickt werden müssten.\n\nEinige Daten könnten jedoch dennoch nach außen geschickt werden. Diese App nutzt \"Crashlytics\" von Google. Dies ist ein Tool, welches Absturzreporte an Google-Server schickt, wenn in der App etwas schief läuft. Diese werden genutzt, um Probleme in der App zu lösen.\n\nDies ist jedoch gemäß europäischer Datenschutz-Grundverordnung Opt-In, was bedeutet, dass Du zustimmen musst, bevor Daten nach außen geschickt werden. Du wurdest danach gefragt, als Du die App zum ersten Mal gestartet hast, und du kannst jederzeit deine Meinung ändern, indem Du den Schalter hierfür in den Einstellungen umstellst. Solltest Du dich entscheiden, keine Daten verschicken zu lassen, wird auch nichts mit Google (und mir) geteilt.\n\nSolltest du dich versichern wollen, dass wirklich keine Daten an irgendwelche Server verschickt wird (abgesehen von den Absturzreports an Google), kannst Du jederzeit das Projekt erkunden, da es Open-Source und auf meinen GitHub-Profil öffentlich zugänglich ist:\nhttps://github.com/denizk0461/studip-timetable/\n\nSchreib mir ansonsten gerne eine E-Mail:\ndenizk0461+kyzil@gmail.com</string>
<string name="settings_app_version">App-Verison</string> <string name="settings_app_version">App-Version</string>
<string name="with_love">mit ♡️ aus Bremen-Osterholz\von denizk0461 \\__*</string> <string name="with_love">mit ♡️ aus Bremen-Osterholz\von denizk0461 \\__*</string>
</resources> </resources>
+3
View File
@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- I am unsure whether these attrs are necessary to define -->
<attr name="scheduleColorDefault" type="color"/> <attr name="scheduleColorDefault" type="color"/>
<attr name="colorOutlineVariant" type="color"/> <attr name="colorOutlineVariant" type="color"/>
<attr name="colorOnSurfaceVariant" type="color"/> <attr name="colorOnSurfaceVariant" type="color"/>
<attr name="colorErrorContainer" type="color"/>
<attr name="colorOnErrorContainer" type="color"/>
</resources> </resources>
+3 -1
View File
@@ -8,6 +8,8 @@
<string name="friday">Friday</string> <string name="friday">Friday</string>
<string name="saturday">Saturday</string> <string name="saturday">Saturday</string>
<string name="sunday">Sunday</string> <string name="sunday">Sunday</string>
<string name="fetch_error_snack">Something went wrong!</string>
<string name="save">Save</string> <string name="save">Save</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
@@ -121,7 +123,7 @@
<string name="settings_data_title">What happens with my data?</string> <string name="settings_data_title">What happens with my data?</string>
<string name="settings_data_subtitle">Will you steal my Stud.IP login?</string> <string name="settings_data_subtitle">Will you steal my Stud.IP login?</string>
<string name="settings_data_sheet_header">TL;DR: no, but Google may let me know if the app crashes on your device.</string> <string name="settings_data_sheet_header">TL;DR: no, but Google may let me know if the app crashes on your device.</string>
<string name="settings_data_sheet_content">This app works standalone, which is to say, it doesn\'t ever connect to a server I own. Instead, it is a web scraper, which means that it opens a website and then downloads and analyses all the data from that website by itself. In this case, those websites are the canteen websites of the Studierendenwerk Bremen (stw-bremen.de) and the Stud.IP page (elearning.uni-bremen.de).\n\nThis works fairly easily for the canteen offers, since the plans are public and anyone can access them. However, to access your personal Stud.IP schedule, the app needs access to that website, with your account logged in. This is done by letting you log into the website by yourself (which happens when you click the button to refresh the schedule and the app opens the login page) and then clicking a button to download the content, the HTML source code, of the website. The HTML source code contains information about which elements are shown on the website and how they look, but it can never contain your personal details such as passwords, or any other sensitive data you may store on your device.\n\nThe HTML source is then processed locally on your device, which means that your device will handle converting the HTML source to the cards you will be shown in the app by itself, without needing to send any data anywhere.\n\nHowever, some data may still be sent outside of your device. This app uses \"Crashlytics\" by Google, which is a tool that sends crash reports to Google servers if something breaks. These are used to fix problems that occur in the app.\n\nThis, in compliance with the European General Data Protection Regulation, is of course opt-in, which means that you need to accept this before any data is sent outside of your device. You have been prompted for this when you first started the app, and you can always change your mind in the app settings by clicking the corresponding switch. Should you choose to not share crash data with Google (and me), no data will be sent outside of your device.\n\nShould you want to reassure yourself that no data is sent to any servers (outside of the crash reports from Google), feel free to explore the project, since it is open-source and documented on my GitHub profile:\nhttps://github.com/denizk0461/studip-timetable/</string> <string name="settings_data_sheet_content">This app works standalone, which is to say, it doesn\'t ever connect to a server I own. Instead, it is a web scraper, which means that it opens a website and then downloads and analyses all the data from that website by itself. In this case, those websites are the canteen websites of the Studierendenwerk Bremen (stw-bremen.de) and the Stud.IP page (elearning.uni-bremen.de).\n\nThis works fairly easily for the canteen offers, since the plans are public and anyone can access them. However, to access your personal Stud.IP schedule, the app needs access to that website, with your account logged in. This is done by letting you log into the website by yourself (which happens when you click the button to refresh the schedule and the app opens the login page) and then clicking a button to download the content, the HTML source code, of the website. The HTML source code contains information about which elements are shown on the website and how they look, but it can never contain your personal details such as passwords, or any other sensitive data you may store on your device.\n\nThe HTML source is then processed locally on your device, which means that your device will handle converting the HTML source to the cards you will be shown in the app by itself, without needing to send any data anywhere.\n\nHowever, some data may still be sent outside of your device. This app uses \"Crashlytics\" by Google, which is a tool that sends crash reports to Google servers if something breaks. These are used to fix problems that occur in the app.\n\nThis, in compliance with the European General Data Protection Regulation, is of course opt-in, which means that you need to accept this before any data is sent outside of your device. You have been prompted for this when you first started the app, and you can always change your mind in the app settings by clicking the corresponding switch. Should you choose to not share crash data with Google (and me), no data will be sent outside of your device.\n\nShould you want to reassure yourself that no data is sent to any servers (outside of the crash reports from Google), feel free to explore the project, since it is open-source and documented on my GitHub profile:\nhttps://github.com/denizk0461/studip-timetable/\n\nYou\'re also welcome to send me an email:\ndenizk0461+kyzil@gmail.com</string>
<string name="settings_app_version">App Version</string> <string name="settings_app_version">App Version</string>