diff --git a/app/build.gradle b/app/build.gradle index 44a57b1..ca6e14e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt index 535a811..cf0a095 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/MainActivity.kt @@ -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") } diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/GradesController.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/GradesController.kt index 65a5740..96b2e79 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/GradesController.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/GradesController.kt @@ -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>, diffMap: HashMap>): ArrayList { + fun diffGrades(origMap: Map>, diffMap: Map>): ArrayList { val diff = ArrayList() 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( + 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") + } + + } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt index 9481817..c029fd4 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/cache/CacheController.kt @@ -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) + } } } diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/preferences/Preferences.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/preferences/Preferences.kt index 0cccc87..b7e8f58 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/controller/preferences/Preferences.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/controller/preferences/Preferences.kt @@ -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 + ) } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/SettingsFragment.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/SettingsFragment.kt index 4599d51..6159d56 100644 --- a/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/SettingsFragment.kt +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/fragments/SettingsFragment.kt @@ -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) } diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/util/NotificationUtils.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/util/NotificationUtils.kt new file mode 100644 index 0000000..dc68bba --- /dev/null +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/util/NotificationUtils.kt @@ -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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mosad/seil0/projectlaogai/worker/GradesUpdateWorker.kt b/app/src/main/java/org/mosad/seil0/projectlaogai/worker/GradesUpdateWorker.kt new file mode 100644 index 0000000..3bea701 --- /dev/null +++ b/app/src/main/java/org/mosad/seil0/projectlaogai/worker/GradesUpdateWorker.kt @@ -0,0 +1,102 @@ +/** + * ProjectLaogai + * + * Copyright 2019-2020 + * + * 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() + } + + + +} \ No newline at end of file diff --git a/app/src/main/res/layouts/fragments/layout/fragment_settings.xml b/app/src/main/res/layouts/fragments/layout/fragment_settings.xml index afb46be..71d1be1 100644 --- a/app/src/main/res/layouts/fragments/layout/fragment_settings.xml +++ b/app/src/main/res/layouts/fragments/layout/fragment_settings.xml @@ -332,6 +332,34 @@ android:layout_height="1dp" android:background="?android:attr/listDivider" /> + + + + + + + + + + diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index cdb941e..985af02 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -66,6 +66,9 @@ Zum Ändern tippen, Standard ist Blaugrün. Akzentfarbe Zum Ändern tippen, Standard ist Hellblau. + Aktualisierungsintervall Noten + %1$d Stunden + nie Buffet immer anzeigen @@ -80,6 +83,12 @@ aktuell: %1$s\n letzte Abbuchung: %1$s + + Noten + %1$s wurden hinzugefügt oder aktualisiert + %1$s und %2$d weiter Vorlesungen wurden hinzugefügt oder aktualisiert + Suche nach neuen Noten … + Fehler Der Stundenplan konnte nicht geladen werden. @@ -101,4 +110,12 @@ Noten Noten deaktiviert + + Manuell + 1 Stunde + 3 Stunden + 6 Stunden + 12 Stunden + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a5aefc8..42105b6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -71,6 +71,9 @@ Tap to change, default is teal blue. Accent color Tap to change, default is light blue. + Update interval grades + %1$d hours + never Always show buffet @@ -85,6 +88,12 @@ current: %1$s\n last: %1$s + + Grades + %1$s was added or updated + %1$s and %2$d other subjects have been added or updated + Checking for new grades … + spinefield@stud.hs-offenburg.de Everything @@ -123,6 +132,7 @@ org.mosad.seil0.projectlaogai.courseTTLink org.mosad.seil0.projectlaogai.colorPrimary org.mosad.seil0.projectlaogai.colorAccent + org.mosad.seil0.projectlaogai.gradesSyncInterval org.mosad.seil0.projectlaogai.showBuffet org.mosad.seil0.projectlaogai.coursesCacheTime org.mosad.seil0.projectlaogai.mensaCacheTime @@ -131,6 +141,13 @@ org.mosad.seil0.projectlaogai.user_email org.mosad.seil0.projectlaogai.user_password + + Manually + 1 Hour + 3 Hours + 6 Hours + 12 Hours + AI-1