Skip to content

Commit 1a404d7

Browse files
committed
Fixes and release 1.3 (v131)
Signed-off-by: koenidv <[email protected]>
1 parent 6ea993d commit 1a404d7

File tree

15 files changed

+184
-80
lines changed

15 files changed

+184
-80
lines changed

.idea/vcs.xml

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@
22
Android app to easily access Hesse's education portal -
33
*SPH: Schulportal Hessen / Education Portal Hesse*
44

5+
<details>
6+
7+
<summary>Screenshots as of 1.2 (v120)</summary>
8+
59
<p float="left">
610
<img src="https://play-lh.googleusercontent.com/W998xiTW5TvKZvtqZFm6hhVLo9ji97VgnDJ9Z86sMJEs53_kA0NU4ohTP9S2W1Pv7Q=w1920-h947-rw" width="200" />
711
<img src="https://play-lh.googleusercontent.com/tYMyPkNZc6qIEbNMNbLksOuiLM9sPncAHFC74Nq-QB1pCetwCEls5baYFo2bhE--tTM=w1920-h947-rw" width="200" />
812
<img src="https://play-lh.googleusercontent.com/htYdoaD9un77stFjNjDeuJ02bA8cTNpwsmv59uv6CWst8yvHsHomIPvpP-VqQfOU5Q=w1920-h947-rw" width="200" />
913
<img src="https://play-lh.googleusercontent.com/W8Buymrj0ShiNaHG3CcXcVh2RcztyA1Tgm4U7xMCqF1cBQzrJyhVLRb89jOZpjXuHA=w1920-h947-rw" width="200" />
1014
</p>
1115

16+
</details>
17+
1218
### Advantages compared to SPH's website
13-
- [x] The App presents way more data at a glance
19+
- [x] Presenting way more data at a glance
1420
- [x] Easier and faster to use
1521
- [x] Skip signing in every time
1622
- [x] Works even when SPH is under maintenance or offline
@@ -19,15 +25,16 @@ Android app to easily access Hesse's education portal -
1925
- [x] Personal timetable (that works better than sph's)
2026
- [x] Personal changes in the timetable
2127
- [x] Overview of undone tasks & unread posts
28+
- [x] Send and receive messages to teachers (starting in 1.3)
2229
- [x] Rename and pin attachments, keep them offline
2330
- [x] Display posts from SPH
2431

2532
### Future plans
26-
- [ ] Implementing the messages feature (wip, decryption works)
27-
- [ ] More reliably support more schools (always wip, developing better debugging features)
33+
- [ ] Notifications
34+
- [ ] Manually edit courses
35+
- [ ] More reliably support more schools (always wip)
2836
- [ ] Displaying grades and attendances
2937
- [ ] Submit files to assignments
30-
- [ ] Manually edit courses
3138
- [ ] Add own tasks / grades / attendances
3239

3340
### Contributing

app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ android {
1212
applicationId "de.koenidv.sph"
1313
minSdkVersion 23
1414
targetSdkVersion 30
15-
versionCode 130
16-
versionName "1.3-dev"
15+
versionCode 131
16+
versionName "1.3"
1717

1818
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1919
}

app/src/main/java/de/koenidv/sph/MainActivity.kt

Lines changed: 76 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,19 @@ import com.google.android.play.core.install.model.InstallStatus
2929
import com.google.android.play.core.install.model.UpdateAvailability
3030
import com.google.android.play.core.tasks.Task
3131
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
32+
import de.koenidv.sph.SphPlanner.Companion.prefs
3233
import de.koenidv.sph.database.ChangesDb
3334
import de.koenidv.sph.database.FunctionTilesDb
3435
import de.koenidv.sph.debugging.Debugger
3536
import de.koenidv.sph.networking.NetworkManager
3637
import de.koenidv.sph.objects.FunctionTile
3738
import de.koenidv.sph.ui.OnboardingActivity
3839
import de.koenidv.sph.ui.OptionsSheet
40+
import kotlinx.coroutines.CoroutineScope
41+
import kotlinx.coroutines.Dispatchers
42+
import kotlinx.coroutines.delay
43+
import kotlinx.coroutines.launch
44+
import java.util.*
3945

4046
// Created by koenidv on 05.12.2020.
4147

@@ -84,6 +90,47 @@ class MainActivity : AppCompatActivity() {
8490
}
8591
}
8692

93+
94+
// If app install is at least 3 days ago,
95+
// the last share snackbar is at least 12 days ago,
96+
// and the app has not been shared before,
97+
// show a snackbar asking the user to share the app after 30 seconds
98+
if (prefs.getLong("install_time", 0) == 0L) {
99+
prefs.edit().putLong("install_time", Date().time).apply()
100+
} else if (Date().time - prefs.getLong("install_time", 0) > 3 * 24 * 360 * 1000 &&
101+
!prefs.getBoolean("share_done", false) &&
102+
Date().time - prefs.getLong("share_snackbar_time", 0) >
103+
12 * 24 * 360 * 1000) {
104+
105+
CoroutineScope(Dispatchers.Main).launch {
106+
delay(30000)
107+
108+
// Create a snackbar with share action
109+
val snackbar = Snackbar.make(
110+
findViewById<FragmentContainerView>(R.id.nav_host_fragment),
111+
R.string.share_prompt, Snackbar.LENGTH_INDEFINITE)
112+
.setAnchorView(R.id.nav_view)
113+
.setAction(R.string.share_action) {
114+
val sendIntent: Intent = Intent().apply {
115+
// Action: Share plain text and save action completion in prefs
116+
action = Intent.ACTION_SEND
117+
putExtra(Intent.EXTRA_TEXT, getString(R.string.share_text))
118+
this.type = "text/plain"
119+
prefs.edit().putBoolean("share_done", true).apply()
120+
}
121+
startActivity(Intent.createChooser(sendIntent, getString(R.string.share_action)))
122+
}
123+
snackbar.show()
124+
prefs.edit().putLong("share_snackbar_time", Date().time).apply()
125+
126+
// Dismiss the snackbar after 10 seconds
127+
delay(10000)
128+
snackbar.dismiss()
129+
130+
}
131+
}
132+
133+
87134
}
88135

89136
override fun onCreate(savedInstanceState: Bundle?) {
@@ -132,36 +179,40 @@ class MainActivity : AppCompatActivity() {
132179
/*
133180
* Pull to refresh
134181
*/
135-
swipeRefresh = findViewById<SwipeRefreshLayout>(R.id.swipeRefresh)
182+
swipeRefresh = findViewById(R.id.swipeRefresh)
136183
swipeRefresh.setOnRefreshListener {
137184
navController.currentDestination?.id?.let { destination ->
138185
// If destination is known, let network manager handle the refreshing
139186
NetworkManager().handlePullToRefresh(destination, lastNavArguments) { success ->
140-
val errorSnackbar = Snackbar.make(findViewById<FragmentContainerView>(R.id.nav_host_fragment), "", Snackbar.LENGTH_LONG)
141-
errorSnackbar.setAnchorView(R.id.nav_view)
142-
// Show error message if needed
143-
when (success) {
144-
NetworkManager.FAILED_NO_NETWORK -> errorSnackbar.setText(R.string.error_offline).show()
145-
NetworkManager.FAILED_MAINTENANCE -> errorSnackbar.setText(R.string.error_maintenance).show()
146-
NetworkManager.FAILED_SERVER_ERROR -> errorSnackbar.setText(R.string.error_server).show()
147-
NetworkManager.FAILED_UNKNOWN, NetworkManager.FAILED_CANCELLED ->
148-
errorSnackbar.setText(R.string.error).show()
149-
else -> if (success != NetworkManager.SUCCESS) errorSnackbar.setText(R.string.error).show()
150-
}
151-
// If this is due to a server error, display a link to sph's status page
152-
if (success == NetworkManager.FAILED_MAINTENANCE
153-
|| success == NetworkManager.FAILED_SERVER_ERROR
154-
|| success == NetworkManager.FAILED_UNKNOWN) {
155-
errorSnackbar.setAction(R.string.sph_status) {
156-
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_status))))
187+
// Just to make sure we're actually running on ui thread after networking
188+
// issue fc-21432b0dd857aa2a3e29e938109bf74c to be specific
189+
runOnUiThread {
190+
val errorSnackbar = Snackbar.make(findViewById<FragmentContainerView>(R.id.nav_host_fragment), "", Snackbar.LENGTH_LONG)
191+
errorSnackbar.setAnchorView(R.id.nav_view)
192+
// Show error message if needed
193+
when (success) {
194+
NetworkManager.FAILED_NO_NETWORK -> errorSnackbar.setText(R.string.error_offline).show()
195+
NetworkManager.FAILED_MAINTENANCE -> errorSnackbar.setText(R.string.error_maintenance).show()
196+
NetworkManager.FAILED_SERVER_ERROR -> errorSnackbar.setText(R.string.error_server).show()
197+
NetworkManager.FAILED_UNKNOWN, NetworkManager.FAILED_CANCELLED ->
198+
errorSnackbar.setText(R.string.error).show()
199+
else -> if (success != NetworkManager.SUCCESS) errorSnackbar.setText(R.string.error).show()
200+
}
201+
// If this is due to a server error, display a link to sph's status page
202+
if (success == NetworkManager.FAILED_MAINTENANCE
203+
|| success == NetworkManager.FAILED_SERVER_ERROR
204+
|| success == NetworkManager.FAILED_UNKNOWN) {
205+
errorSnackbar.setAction(R.string.sph_status) {
206+
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_status))))
207+
}
208+
}
209+
// Indicate no longer refreshing
210+
swipeRefresh.isRefreshing = false
211+
if (success == NetworkManager.FAILED_INVALID_CREDENTIALS) {
212+
// Credentials seem to be invalid
213+
// Show sign in screen
214+
prefs.edit().remove("credsVerified").apply()
157215
}
158-
}
159-
// Indicate no longer refreshing
160-
swipeRefresh.isRefreshing = false
161-
if (success == NetworkManager.FAILED_INVALID_CREDENTIALS) {
162-
// Credentials seem to be invalid
163-
// Show sign in screen
164-
prefs.edit().remove("credsVerified").apply()
165216
}
166217
}
167218
}

app/src/main/java/de/koenidv/sph/database/DatabaseHelper.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
public class DatabaseHelper extends SQLiteOpenHelper {
1212

1313
// todo close db on exit
14-
// todo escape everything we put in the db
1514

1615
private static DatabaseHelper instance;
1716

app/src/main/java/de/koenidv/sph/database/TimetableDb.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,21 @@ class TimetableDb private constructor() {
6262
// If there are no courses, return an empty list
6363
if (unorderedList.isNullOrEmpty()) return listOf()
6464
// Create a list for each day containing as many lesson lists as the timetable has maximum hours per day
65-
val orderedList: List<List<MutableList<TimetableEntry>>> = List(5) { day: Int ->
66-
List(unorderedList.filter { it.day == day }.maxOf { it.hour }) { mutableListOf() }
65+
val orderedList: List<List<MutableList<TimetableEntry>>> = try {
66+
List(5) { day: Int ->
67+
List(try {
68+
// Get the maximum number of lessons for this day
69+
unorderedList.filter { it.day == day }.maxOf { it.hour }
70+
} catch (nse: NoSuchElementException) {
71+
// If there are no lessons for this day
72+
0
73+
}) {
74+
// Mutable list for each hour per day
75+
mutableListOf()
76+
}
77+
}
78+
} catch (nse: NoSuchElementException) {
79+
List(5) { listOf(mutableListOf()) }
6780
}
6881
// Map each lesson to the corresponding day / hour list
6982
unorderedList.map {

app/src/main/java/de/koenidv/sph/networking/Messages.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import com.google.gson.JsonArray
99
import com.google.gson.JsonObject
1010
import com.google.gson.JsonParser
1111
import de.koenidv.sph.R
12-
import de.koenidv.sph.SphPlanner
1312
import de.koenidv.sph.SphPlanner.Companion.TAG
1413
import de.koenidv.sph.SphPlanner.Companion.applicationContext
14+
import de.koenidv.sph.SphPlanner.Companion.prefs
1515
import de.koenidv.sph.database.ConversationsDb
1616
import de.koenidv.sph.database.MessagesDb
1717
import de.koenidv.sph.database.UsersDb
@@ -68,13 +68,14 @@ class Messages {
6868
// last=0 - Not yet sure what that does, but it is needed to not get an error
6969
NetworkManager().postJsonAuthed(applicationContext().getString(R.string.url_messages),
7070
body = mapOf("a" to "headers", "getType" to typeBody, "last" to "0")) { netSuccess, json ->
71-
if (netSuccess == NetworkManager.SUCCESS && json != null) {
71+
if (netSuccess == NetworkManager.SUCCESS && json != null &&
72+
json.get("rows") != "false") {
7273
// The response should be a json object with two values:
7374
// total - The number of messages matching our request
7475
// rows - The encrypted headers for each message
7576

7677
cryption.decrypt(json.get("rows").toString()) { headers ->
77-
if (headers != null) {
78+
if (headers != null && headers != "") {
7879
val conversations = ConversationsDb()
7980

8081
var data: JsonObject
@@ -191,7 +192,7 @@ class Messages {
191192
// Remember the time we updated this
192193
// Not updating this the first time,
193194
// but that shouldn't be an issue
194-
SphPlanner.prefs.edit().putLong("updated_messages",
195+
prefs.edit().putLong("updated_messages",
195196
Date().time).apply()
196197
} else {
197198
var index = 0
@@ -214,8 +215,13 @@ class Messages {
214215
} else {
215216
// For some reason the decrypted data is null
216217
callback(NetworkManager.FAILED_UNKNOWN)
218+
prefs.edit().putLong("cryption_time", 0).apply()
217219
}
218220
}
221+
} else {
222+
// The provided data is invalid
223+
callback(NetworkManager.FAILED_UNKNOWN)
224+
prefs.edit().putLong("cryption_time", 0).apply()
219225
}
220226
}
221227
}
@@ -371,7 +377,7 @@ class Messages {
371377
notifyFragments(conversationId, "contentchanged")
372378

373379
// Remember the time we updated this
374-
SphPlanner.prefs.edit().putLong(
380+
prefs.edit().putLong(
375381
"updated_messages_$conversationId", Date().time).apply()
376382
}
377383
}
@@ -495,7 +501,7 @@ class Messages {
495501
conversation.convId,
496502
TokenManager.userid,
497503
Message.SENDER_TYPE_STUDENT,
498-
SphPlanner.prefs.getString("real_name", "")!!,
504+
prefs.getString("real_name", "")!!,
499505
Date(), // A few seconds later than actual, shouldn't be an issue
500506
conversation.subject,
501507
message,

app/src/main/java/de/koenidv/sph/parsing/CourseInfo.kt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,11 @@ object CourseInfo {
2020
*/
2121
fun getFullnameFromInternald(internalCourseId: String): String {
2222
// Get subject from course id
23-
val subject = if (internalCourseId.contains("_"))
24-
internalCourseId.substring(0, internalCourseId.indexOf("_"))
25-
else internalCourseId
23+
val subject = internalCourseId.substringBefore("_")
2624
// Get a map of full names
2725
val nameMap = Utility.parseStringArray(R.array.courseId_fullName)
2826
// Return map value if available, else subject
29-
return if (nameMap.containsKey(subject)) nameMap.getValue(subject)
30-
else subject.capitalize(Locale.getDefault())
27+
return nameMap.getOrElse(subject, { subject.capitalize(Locale.getDefault()) })
3128
}
3229

3330
/**
@@ -37,7 +34,7 @@ object CourseInfo {
3734
// Check if the provided id is in fact an internal one
3835
require(IdParser().getCourseIdType(courseId) == TYPE_INTERNAL)
3936
// Get subject from course id
40-
val subject = courseId.substring(0, courseId.indexOf("_"))
37+
val subject = courseId.substringBefore("_")
4138
// Get a map of short and full names
4239
val shortMap = Utility.parseStringArray(R.array.courseId_shortName_override)
4340
val nameMap = Utility.parseStringArray(R.array.courseId_fullName)

app/src/main/java/de/koenidv/sph/parsing/RawParser.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class RawParser {
3838
val dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.GERMAN)
3939

4040
// We'll need those below to construct our Change object
41+
var dateString: String?
4142
var date: Date
4243
var internalId: String?
4344
var id_course_external: String? = null
@@ -54,10 +55,12 @@ class RawParser {
5455
// Extract every changes table, i.e. every available day
5556
for (panel in doc.select("div.panel:not(#menue_tag) div.panel-body")) {
5657
// Parse date
57-
// Substring: Only get "11.01.2021" from "Vertretungen am 11.01.2021" -> Chars 16-26
58-
date = dateFormat.parse(
59-
panel.selectFirst("h3").text()
60-
.substring(16, 26))!!
58+
// Only get "11.01.2021" from "Vertretungen am 11.01.2021" -> Chars 16-26
59+
dateString = Regex("""\d{1,2}\.\d{1,2}\.\d{2,4}""")
60+
.find(panel.selectFirst("h3").text())?.value
61+
date = if (dateString != null)
62+
dateFormat.parse(dateString)!!
63+
else Date() // Use current date as fallback if no date was found
6164

6265
// We are left with a table, each row contains these columns:
6366
// title (not needed), lessons (11 11 - 12), classname (Q34), old classname (Q34, mostly empty),

0 commit comments

Comments
 (0)