add background grades updates with notify on changed grades
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Jannik 2020-08-31 23:03:58 +02:00
parent 95ce9e14bd
commit a3102bc3f2
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
11 changed files with 342 additions and 28 deletions

View File

@ -11,8 +11,8 @@ android {
applicationId "org.mosad.seil0.projectlaogai"
minSdkVersion 23
targetSdkVersion 29
versionCode 6000 // 0006000
versionName "0.6.0"
versionCode 6090 // 0006000
versionName "0.6.1-beta1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime()
setProperty("archivesBaseName", "projectlaogai-$versionName")
@ -31,6 +31,10 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
sourceSets {
main {
res.srcDirs =
@ -63,6 +67,8 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.security:security-crypto:1.1.0-alpha02'
implementation 'androidx.work:work-runtime:2.4.0'
implementation "androidx.work:work-runtime-ktx:2.4.0"
implementation 'com.google.android.material:material:1.2.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.afollestad:aesthetic:1.0.0-beta05'

View File

@ -51,6 +51,7 @@ import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cColorAccent
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cColorPrimary
import org.mosad.seil0.projectlaogai.fragments.*
import org.mosad.seil0.projectlaogai.util.NotificationUtils
import kotlin.system.measureTimeMillis
/**
@ -179,6 +180,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
Preferences.load(this) // load the settings, must be finished before doing anything else
CacheController(this) // load the cache
EncryptedPreferences.load(this)
NotificationUtils(this)
}
Log.i(className, "startup completed in $startupTime ms")
}

View File

@ -22,14 +22,21 @@
package org.mosad.seil0.projectlaogai.controller
import android.content.Context
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
import org.mosad.seil0.projectlaogai.util.GradeSubject
import org.mosad.seil0.projectlaogai.worker.GradesUpdateWorker
import java.util.concurrent.TimeUnit
class GradesController {
/**
* show the difference between 2 grades sets
*/
fun diffGrades(origMap: HashMap<String, ArrayList<GradeSubject>>, diffMap: HashMap<String, ArrayList<GradeSubject>>): ArrayList<GradeSubject> {
fun diffGrades(origMap: Map<String, ArrayList<GradeSubject>>, diffMap: Map<String, ArrayList<GradeSubject>>): ArrayList<GradeSubject> {
val diff = ArrayList<GradeSubject>()
diffMap.values.forEach { semester ->
@ -54,6 +61,39 @@ class GradesController {
return diff
}
companion object {
/**
* stop the grades background sync, if the sync interval is greater than 0
* start it again with the new interval
*/
fun updateBackgroundSync(context: Context) {
stopBackgroundSync(context)
// if interval is not 0, start background Sync
if (Preferences.gradesSyncInterval > 0)
startBackgroundSync(context)
}
/**
* start a new periodic worker GradesUpdateWorker
*/
fun startBackgroundSync(context: Context) {
val work = PeriodicWorkRequestBuilder<GradesUpdateWorker>(
Preferences.gradesSyncInterval.toLong(),
TimeUnit.HOURS
).build()
val workManager = WorkManager.getInstance(context)
workManager.enqueueUniquePeriodicWork("GradesUpdateWorker", ExistingPeriodicWorkPolicy.REPLACE, work)
}
/**
* cancel GradesUpdateWorker
*/
fun stopBackgroundSync(context: Context) {
WorkManager.getInstance(context).cancelUniqueWork("GradesUpdateWorker")
}
}
}

View File

@ -215,8 +215,6 @@ class CacheController(cont: Context) {
return GlobalScope.launch(Dispatchers.IO) {
if (parser.checkQISPOSStatus() == 200) {
if (file.exists()) { file.delete() }
// save cache file and update time
saveEncrypted(context, file, Gson().toJson(parser.parseGrades()))
gradesCacheTime = System.currentTimeMillis() / 1000
@ -258,8 +256,13 @@ class CacheController(cont: Context) {
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
encryptedFile.openFileOutput().bufferedWriter().use {
it.write(text)
// only write one file at a time
synchronized(this) {
if (file.exists()) { file.delete() }
encryptedFile.openFileOutput().bufferedWriter().use {
it.write(text)
}
}
}

View File

@ -39,6 +39,7 @@ object Preferences {
var gradesCacheTime: Long = 0
var cColorPrimary: Int = Color.parseColor("#009688")
var cColorAccent: Int = Color.parseColor("#0096ff")
var gradesSyncInterval = 0
var cCourse = Course(
"https://www.hs-offenburg.de/index.php?id=6627&class=class&iddV=DA64F6FE-9DDB-429E-A677-05D0D40CB636&week=0",
"AI3"
@ -132,6 +133,22 @@ object Preferences {
cColorAccent = colorAccent
}
fun saveGradesSync(context: Context, interval: Int) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
with (sharedPref.edit()) {
putInt(context.getString(R.string.save_key_gradesSyncInterval),
interval
)
apply()
}
gradesSyncInterval = interval
}
/**
* save showBuffet
*/
@ -161,18 +178,18 @@ object Preferences {
)
// load the update times (cache)
coursesCacheTime = sharedPref.getLong(context.getString(
R.string.save_key_coursesCacheTime
), 0)
mensaCacheTime = sharedPref.getLong(context.getString(
R.string.save_key_mensaCacheTime
), 0)
timetableCacheTime = sharedPref.getLong(context.getString(
R.string.save_key_timetableCacheTime
), 0)
gradesCacheTime = sharedPref.getLong(context.getString(
R.string.save_key_gradesCacheTime
), 0)
coursesCacheTime = sharedPref.getLong(
context.getString(R.string.save_key_coursesCacheTime),0
)
mensaCacheTime = sharedPref.getLong(
context.getString(R.string.save_key_mensaCacheTime),0
)
timetableCacheTime = sharedPref.getLong(
context.getString(R.string.save_key_timetableCacheTime),0
)
gradesCacheTime = sharedPref.getLong(
context.getString(R.string.save_key_gradesCacheTime), 0
)
// load saved course
cCourse = Course(
@ -183,17 +200,22 @@ object Preferences {
)
// load saved colors
cColorPrimary = sharedPref.getInt(context.getString(
R.string.save_key_colorPrimary
), cColorPrimary)
cColorAccent = sharedPref.getInt(context.getString(
R.string.save_key_colorAccent
), cColorAccent)
cColorPrimary = sharedPref.getInt(
context.getString(R.string.save_key_colorPrimary), cColorPrimary
)
cColorAccent = sharedPref.getInt(
context.getString(R.string.save_key_colorAccent), cColorAccent
)
// load grades sync interval
gradesSyncInterval = sharedPref.getInt(
context.getString(R.string.save_key_gradesSyncInterval), gradesSyncInterval
)
// load showBuffet
cShowBuffet = sharedPref.getBoolean(context.getString(
R.string.save_key_showBuffet
), true)
cShowBuffet = sharedPref.getBoolean(
context.getString(R.string.save_key_showBuffet), true
)
}
}

View File

@ -47,6 +47,7 @@ import kotlinx.android.synthetic.main.fragment_settings.*
import kotlinx.coroutines.*
import org.mosad.seil0.projectlaogai.BuildConfig
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.controller.GradesController
import org.mosad.seil0.projectlaogai.controller.cache.CacheController
import org.mosad.seil0.projectlaogai.controller.cache.CacheController.Companion.coursesList
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
@ -74,6 +75,7 @@ class SettingsFragment : Fragment() {
private lateinit var linLayoutTheme: LinearLayout
private lateinit var linLayoutPrimaryColor: LinearLayout
private lateinit var linLayoutAccentColor: LinearLayout
private lateinit var linLayoutGradesSync: LinearLayout
private lateinit var switchBuffet: SwitchCompat
private lateinit var txtViewCourse: TextView
@ -97,6 +99,7 @@ class SettingsFragment : Fragment() {
linLayoutTheme = view.findViewById(R.id.linLayout_Theme)
linLayoutPrimaryColor = view.findViewById(R.id.linLayout_PrimaryColor)
linLayoutAccentColor = view.findViewById(R.id.linLayout_AccentColor)
linLayoutGradesSync = view.findViewById(R.id.linLayout_GradesSync)
switchBuffet = view.findViewById(R.id.switch_buffet)
// if we call txtView_Course via KAE view binding it'll result in a NPE in the onDismissed call
@ -108,6 +111,11 @@ class SettingsFragment : Fragment() {
txtView_User.text = EncryptedPreferences.email.ifEmpty { resources.getString(R.string.sample_user) }
txtView_Course.text = cCourse.courseName
txtView_AboutDesc.text = resources.getString(R.string.about_version, BuildConfig.VERSION_NAME, getString(R.string.build_time))
txtView_GradesSyncDesc.text = if (Preferences.gradesSyncInterval == 0) {
resources.getString(R.string.grades_sync_desc_never)
} else {
resources.getString(R.string.grades_sync_desc, Preferences.gradesSyncInterval)
}
switch_buffet.isChecked = cShowBuffet // init switch
val outValue = TypedValue()
@ -281,6 +289,38 @@ class SettingsFragment : Fragment() {
}
}
linLayoutGradesSync.setOnClickListener {
val items = resources.getStringArray(R.array.syncInterval).toList()
val initial = when (Preferences.gradesSyncInterval) {
1 -> 1
3 -> 2
6 -> 3
12 -> 4
else -> 0
}
MaterialDialog(context!!)
.title(R.string.grades_sync)
.listItemsSingleChoice(items = items, initialSelection = initial) { _, index, _ ->
val interval = when (index) {
1 -> 1
2 -> 3
3 -> 6
4 -> 12
else -> 0
}
Preferences.saveGradesSync(context!!, interval)
GradesController.updateBackgroundSync(context!!)
txtView_GradesSyncDesc.text = if (Preferences.gradesSyncInterval == 0) {
resources.getString(R.string.grades_sync_desc_never)
} else {
resources.getString(R.string.grades_sync_desc, Preferences.gradesSyncInterval)
}
}.show()
}
switchBuffet.setOnClickListener {
Preferences.saveShowBuffet(context!!, switchBuffet.isChecked)
}

View File

@ -0,0 +1,36 @@
package org.mosad.seil0.projectlaogai.util
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import java.util.concurrent.atomic.AtomicInteger
class NotificationUtils(val context: Context) {
companion object {
val CHANNEL_ID_GRADES = "channel_grades"
val id = AtomicInteger(0)
fun getId() = id.incrementAndGet()
}
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(CHANNEL_ID_GRADES, "Grades Channel", "A Channel")
}
}
@RequiresApi(26)
private fun createNotificationChannel(channelId: String, name: String, desc: String) {
val mChannel = NotificationChannel(channelId, name, NotificationManager.IMPORTANCE_DEFAULT)
mChannel.description = desc
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(mChannel)
}
}

View File

@ -0,0 +1,102 @@
/**
* ProjectLaogai
*
* Copyright 2019-2020 <seil0@mosad.xyz>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
*/
package org.mosad.seil0.projectlaogai.worker
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.Worker
import androidx.work.WorkerParameters
import kotlinx.coroutines.runBlocking
import org.mosad.seil0.projectlaogai.R
import org.mosad.seil0.projectlaogai.controller.GradesController
import org.mosad.seil0.projectlaogai.controller.cache.CacheController
import org.mosad.seil0.projectlaogai.controller.preferences.EncryptedPreferences
import org.mosad.seil0.projectlaogai.util.NotificationUtils
import org.mosad.seil0.projectlaogai.util.NotificationUtils.Companion.CHANNEL_ID_GRADES
class GradesUpdateWorker(val context: Context, params: WorkerParameters): Worker(context, params) {
override fun doWork(): Result {
// check if credentials are present, if not do nothing
val credentials = EncryptedPreferences.readCredentials(context)
if (credentials.first.isEmpty() || credentials.second.isEmpty()) {
return Result.success()
}
// TODO show updating notification, for debugging
println("doing work ...")
val notificationIdDBG = NotificationUtils.getId()
val builderDBG = NotificationCompat.Builder(context, CHANNEL_ID_GRADES)
.setSmallIcon(R.drawable.ic_grading_black_24dp)
.setContentTitle(context.getString(R.string.notification_grades))
.setContentText(context.getString(R.string.notification_grades_updating_desc))
.setNotificationSilent()
NotificationManagerCompat.from(context).apply {
notify(notificationIdDBG, builderDBG.build())
}
// get old grades
val oldGrades = CacheController(context).readGrades()
// get update from qispos
runBlocking { CacheController.updateGrades(context).join() }
val newGrades = CacheController(context).readGrades()
// check for changes
val diff = GradesController().diffGrades(oldGrades, newGrades)
// show message
if (diff.isNotEmpty()) {
val text = if (diff.size < 2) {
context.getString(R.string.notification_grades_single_desc, diff.first().name)
} else {
context.getString(R.string.notification_grades_multiple_desc, diff.first().name, (diff.size - 1))
}
val builder = NotificationCompat.Builder(context, CHANNEL_ID_GRADES)
.setSmallIcon(R.drawable.ic_grading_black_24dp)
.setContentTitle(context.getString(R.string.notification_grades))
.setContentText(text)
// if there are multiple subjects, use BigText
if (diff.size > 1)
builder.setStyle(NotificationCompat.BigTextStyle().bigText(text))
NotificationManagerCompat.from(context).apply {
notify(NotificationUtils.getId(), builder.build())
}
}
// TODO remove debug notification
NotificationManagerCompat.from(context).apply {
cancel(notificationIdDBG)
}
return Result.success()
}
}

View File

@ -332,6 +332,34 @@
android:layout_height="1dp"
android:background="?android:attr/listDivider" />
<LinearLayout
android:id="@+id/linLayout_GradesSync"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="7dp"
android:orientation="vertical">
<TextView
android:id="@+id/txtView_GradesSync"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/grades_sync"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtView_GradesSyncDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/grades_sync_desc" />
</LinearLayout>
<View
android:id="@+id/divider8"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_buffet"
android:layout_width="match_parent"
@ -340,6 +368,7 @@
android:text="@string/show_buffet"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>

View File

@ -66,6 +66,9 @@
<string name="primary_color_desc">Zum Ändern tippen, Standard ist Blaugrün.</string>
<string name="accent_color">Akzentfarbe</string>
<string name="accent_color_desc">Zum Ändern tippen, Standard ist Hellblau.</string>
<string name="grades_sync">Aktualisierungsintervall Noten</string>
<string name="grades_sync_desc">%1$d Stunden</string>
<string name="grades_sync_desc_never">nie</string>
<string name="show_buffet">Buffet immer anzeigen</string>
<!-- dialogs -->
@ -80,6 +83,12 @@
<string name="mensa_current">aktuell: %1$s\n</string>
<string name="mensa_last">letzte Abbuchung: %1$s</string>
<!-- notifications -->
<string name="notification_grades">Noten</string>
<string name="notification_grades_single_desc">%1$s wurden hinzugefügt oder aktualisiert</string>
<string name="notification_grades_multiple_desc">%1$s und %2$d weiter Vorlesungen wurden hinzugefügt oder aktualisiert</string>
<string name="notification_grades_updating_desc">Suche nach neuen Noten …</string>
<!-- errors -->
<string name="error">Fehler</string>
<string name="timetable_error">Der Stundenplan konnte nicht geladen werden.</string>
@ -101,4 +110,12 @@
<string name="shortcut_grades_long">Noten</string>
<string name="shortcut_grades_disabled">Noten deaktiviert</string>
<string-array name="syncInterval">
<item>Manuell</item>
<item>1 Stunde</item>
<item>3 Stunden</item>
<item>6 Stunden</item>
<item>12 Stunden</item>
</string-array>
</resources>

View File

@ -71,6 +71,9 @@
<string name="primary_color_desc">Tap to change, default is teal blue.</string>
<string name="accent_color">Accent color</string>
<string name="accent_color_desc">Tap to change, default is light blue.</string>
<string name="grades_sync">Update interval grades</string>
<string name="grades_sync_desc">%1$d hours</string>
<string name="grades_sync_desc_never">never</string>
<string name="show_buffet">Always show buffet</string>
<!-- dialogs -->
@ -85,6 +88,12 @@
<string name="mensa_current">current: %1$s\n</string>
<string name="mensa_last">last: %1$s</string>
<!-- notifications -->
<string name="notification_grades">Grades</string>
<string name="notification_grades_single_desc">%1$s was added or updated</string>
<string name="notification_grades_multiple_desc">%1$s and %2$d other subjects have been added or updated</string>
<string name="notification_grades_updating_desc">Checking for new grades …</string>
<!-- sample strings -->
<string name="sample_user" translatable="false">spinefield@stud.hs-offenburg.de</string>
<string name="sample_course" translatable="false">Everything</string>
@ -123,6 +132,7 @@
<string name="save_key_courseTTLink" translatable="false">org.mosad.seil0.projectlaogai.courseTTLink</string>
<string name="save_key_colorPrimary" translatable="false">org.mosad.seil0.projectlaogai.colorPrimary</string>
<string name="save_key_colorAccent" translatable="false">org.mosad.seil0.projectlaogai.colorAccent</string>
<string name="save_key_gradesSyncInterval" translatable="false">org.mosad.seil0.projectlaogai.gradesSyncInterval</string>
<string name="save_key_showBuffet" translatable="false">org.mosad.seil0.projectlaogai.showBuffet</string>
<string name="save_key_coursesCacheTime" translatable="false">org.mosad.seil0.projectlaogai.coursesCacheTime</string>
<string name="save_key_mensaCacheTime" translatable="false">org.mosad.seil0.projectlaogai.mensaCacheTime</string>
@ -131,6 +141,13 @@
<string name="save_key_user_email" translatable="false">org.mosad.seil0.projectlaogai.user_email</string>
<string name="save_key_user_password" translatable="false">org.mosad.seil0.projectlaogai.user_password</string>
<string-array name="syncInterval">
<item>Manually</item>
<item>1 Hour</item>
<item>3 Hours</item>
<item>6 Hours</item>
<item>12 Hours</item>
</string-array>
<string-array name="courses">
<item>AI-1</item>