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" applicationId "org.mosad.seil0.projectlaogai"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 29 targetSdkVersion 29
versionCode 6000 // 0006000 versionCode 6090 // 0006000
versionName "0.6.0" versionName "0.6.1-beta1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime() resValue "string", "build_time", buildTime()
setProperty("archivesBaseName", "projectlaogai-$versionName") setProperty("archivesBaseName", "projectlaogai-$versionName")
@ -31,6 +31,10 @@ android {
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
sourceSets { sourceSets {
main { main {
res.srcDirs = res.srcDirs =
@ -63,6 +67,8 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.0.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.security:security-crypto:1.1.0-alpha02' 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.android.material:material:1.2.0'
implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.afollestad:aesthetic:1.0.0-beta05' 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.cColorAccent
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cColorPrimary import org.mosad.seil0.projectlaogai.controller.preferences.Preferences.cColorPrimary
import org.mosad.seil0.projectlaogai.fragments.* import org.mosad.seil0.projectlaogai.fragments.*
import org.mosad.seil0.projectlaogai.util.NotificationUtils
import kotlin.system.measureTimeMillis 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 Preferences.load(this) // load the settings, must be finished before doing anything else
CacheController(this) // load the cache CacheController(this) // load the cache
EncryptedPreferences.load(this) EncryptedPreferences.load(this)
NotificationUtils(this)
} }
Log.i(className, "startup completed in $startupTime ms") Log.i(className, "startup completed in $startupTime ms")
} }

View File

@ -22,14 +22,21 @@
package org.mosad.seil0.projectlaogai.controller 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.util.GradeSubject
import org.mosad.seil0.projectlaogai.worker.GradesUpdateWorker
import java.util.concurrent.TimeUnit
class GradesController { class GradesController {
/** /**
* show the difference between 2 grades sets * 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>() val diff = ArrayList<GradeSubject>()
diffMap.values.forEach { semester -> diffMap.values.forEach { semester ->
@ -54,6 +61,39 @@ class GradesController {
return diff 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) { return GlobalScope.launch(Dispatchers.IO) {
if (parser.checkQISPOSStatus() == 200) { if (parser.checkQISPOSStatus() == 200) {
if (file.exists()) { file.delete() }
// save cache file and update time // save cache file and update time
saveEncrypted(context, file, Gson().toJson(parser.parseGrades())) saveEncrypted(context, file, Gson().toJson(parser.parseGrades()))
gradesCacheTime = System.currentTimeMillis() / 1000 gradesCacheTime = System.currentTimeMillis() / 1000
@ -258,8 +256,13 @@ class CacheController(cont: Context) {
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build() ).build()
encryptedFile.openFileOutput().bufferedWriter().use { // only write one file at a time
it.write(text) 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 gradesCacheTime: Long = 0
var cColorPrimary: Int = Color.parseColor("#009688") var cColorPrimary: Int = Color.parseColor("#009688")
var cColorAccent: Int = Color.parseColor("#0096ff") var cColorAccent: Int = Color.parseColor("#0096ff")
var gradesSyncInterval = 0
var cCourse = Course( var cCourse = Course(
"https://www.hs-offenburg.de/index.php?id=6627&class=class&iddV=DA64F6FE-9DDB-429E-A677-05D0D40CB636&week=0", "https://www.hs-offenburg.de/index.php?id=6627&class=class&iddV=DA64F6FE-9DDB-429E-A677-05D0D40CB636&week=0",
"AI3" "AI3"
@ -132,6 +133,22 @@ object Preferences {
cColorAccent = colorAccent 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 * save showBuffet
*/ */
@ -161,18 +178,18 @@ object Preferences {
) )
// load the update times (cache) // load the update times (cache)
coursesCacheTime = sharedPref.getLong(context.getString( coursesCacheTime = sharedPref.getLong(
R.string.save_key_coursesCacheTime context.getString(R.string.save_key_coursesCacheTime),0
), 0) )
mensaCacheTime = sharedPref.getLong(context.getString( mensaCacheTime = sharedPref.getLong(
R.string.save_key_mensaCacheTime context.getString(R.string.save_key_mensaCacheTime),0
), 0) )
timetableCacheTime = sharedPref.getLong(context.getString( timetableCacheTime = sharedPref.getLong(
R.string.save_key_timetableCacheTime context.getString(R.string.save_key_timetableCacheTime),0
), 0) )
gradesCacheTime = sharedPref.getLong(context.getString( gradesCacheTime = sharedPref.getLong(
R.string.save_key_gradesCacheTime context.getString(R.string.save_key_gradesCacheTime), 0
), 0) )
// load saved course // load saved course
cCourse = Course( cCourse = Course(
@ -183,17 +200,22 @@ object Preferences {
) )
// load saved colors // load saved colors
cColorPrimary = sharedPref.getInt(context.getString( cColorPrimary = sharedPref.getInt(
R.string.save_key_colorPrimary context.getString(R.string.save_key_colorPrimary), cColorPrimary
), cColorPrimary) )
cColorAccent = sharedPref.getInt(context.getString( cColorAccent = sharedPref.getInt(
R.string.save_key_colorAccent context.getString(R.string.save_key_colorAccent), cColorAccent
), cColorAccent) )
// load grades sync interval
gradesSyncInterval = sharedPref.getInt(
context.getString(R.string.save_key_gradesSyncInterval), gradesSyncInterval
)
// load showBuffet // load showBuffet
cShowBuffet = sharedPref.getBoolean(context.getString( cShowBuffet = sharedPref.getBoolean(
R.string.save_key_showBuffet context.getString(R.string.save_key_showBuffet), true
), true) )
} }
} }

View File

@ -47,6 +47,7 @@ import kotlinx.android.synthetic.main.fragment_settings.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.mosad.seil0.projectlaogai.BuildConfig import org.mosad.seil0.projectlaogai.BuildConfig
import org.mosad.seil0.projectlaogai.R 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
import org.mosad.seil0.projectlaogai.controller.cache.CacheController.Companion.coursesList import org.mosad.seil0.projectlaogai.controller.cache.CacheController.Companion.coursesList
import org.mosad.seil0.projectlaogai.controller.preferences.Preferences import org.mosad.seil0.projectlaogai.controller.preferences.Preferences
@ -74,6 +75,7 @@ class SettingsFragment : Fragment() {
private lateinit var linLayoutTheme: LinearLayout private lateinit var linLayoutTheme: LinearLayout
private lateinit var linLayoutPrimaryColor: LinearLayout private lateinit var linLayoutPrimaryColor: LinearLayout
private lateinit var linLayoutAccentColor: LinearLayout private lateinit var linLayoutAccentColor: LinearLayout
private lateinit var linLayoutGradesSync: LinearLayout
private lateinit var switchBuffet: SwitchCompat private lateinit var switchBuffet: SwitchCompat
private lateinit var txtViewCourse: TextView private lateinit var txtViewCourse: TextView
@ -97,6 +99,7 @@ class SettingsFragment : Fragment() {
linLayoutTheme = view.findViewById(R.id.linLayout_Theme) linLayoutTheme = view.findViewById(R.id.linLayout_Theme)
linLayoutPrimaryColor = view.findViewById(R.id.linLayout_PrimaryColor) linLayoutPrimaryColor = view.findViewById(R.id.linLayout_PrimaryColor)
linLayoutAccentColor = view.findViewById(R.id.linLayout_AccentColor) linLayoutAccentColor = view.findViewById(R.id.linLayout_AccentColor)
linLayoutGradesSync = view.findViewById(R.id.linLayout_GradesSync)
switchBuffet = view.findViewById(R.id.switch_buffet) 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 // 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_User.text = EncryptedPreferences.email.ifEmpty { resources.getString(R.string.sample_user) }
txtView_Course.text = cCourse.courseName txtView_Course.text = cCourse.courseName
txtView_AboutDesc.text = resources.getString(R.string.about_version, BuildConfig.VERSION_NAME, getString(R.string.build_time)) 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 switch_buffet.isChecked = cShowBuffet // init switch
val outValue = TypedValue() 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 { switchBuffet.setOnClickListener {
Preferences.saveShowBuffet(context!!, switchBuffet.isChecked) 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:layout_height="1dp"
android:background="?android:attr/listDivider" /> 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 <androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_buffet" android:id="@+id/switch_buffet"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -340,6 +368,7 @@
android:text="@string/show_buffet" android:text="@string/show_buffet"
android:textSize="16sp" android:textSize="16sp"
android:textStyle="bold" /> android:textStyle="bold" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
</LinearLayout> </LinearLayout>

View File

@ -66,6 +66,9 @@
<string name="primary_color_desc">Zum Ändern tippen, Standard ist Blaugrün.</string> <string name="primary_color_desc">Zum Ändern tippen, Standard ist Blaugrün.</string>
<string name="accent_color">Akzentfarbe</string> <string name="accent_color">Akzentfarbe</string>
<string name="accent_color_desc">Zum Ändern tippen, Standard ist Hellblau.</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> <string name="show_buffet">Buffet immer anzeigen</string>
<!-- dialogs --> <!-- dialogs -->
@ -80,6 +83,12 @@
<string name="mensa_current">aktuell: %1$s\n</string> <string name="mensa_current">aktuell: %1$s\n</string>
<string name="mensa_last">letzte Abbuchung: %1$s</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 --> <!-- errors -->
<string name="error">Fehler</string> <string name="error">Fehler</string>
<string name="timetable_error">Der Stundenplan konnte nicht geladen werden.</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_long">Noten</string>
<string name="shortcut_grades_disabled">Noten deaktiviert</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> </resources>

View File

@ -71,6 +71,9 @@
<string name="primary_color_desc">Tap to change, default is teal blue.</string> <string name="primary_color_desc">Tap to change, default is teal blue.</string>
<string name="accent_color">Accent color</string> <string name="accent_color">Accent color</string>
<string name="accent_color_desc">Tap to change, default is light blue.</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> <string name="show_buffet">Always show buffet</string>
<!-- dialogs --> <!-- dialogs -->
@ -85,6 +88,12 @@
<string name="mensa_current">current: %1$s\n</string> <string name="mensa_current">current: %1$s\n</string>
<string name="mensa_last">last: %1$s</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 --> <!-- sample strings -->
<string name="sample_user" translatable="false">spinefield@stud.hs-offenburg.de</string> <string name="sample_user" translatable="false">spinefield@stud.hs-offenburg.de</string>
<string name="sample_course" translatable="false">Everything</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_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_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_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_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_coursesCacheTime" translatable="false">org.mosad.seil0.projectlaogai.coursesCacheTime</string>
<string name="save_key_mensaCacheTime" translatable="false">org.mosad.seil0.projectlaogai.mensaCacheTime</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_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 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"> <string-array name="courses">
<item>AI-1</item> <item>AI-1</item>