Started converting the app to a true MVVM architecture; fixed 7 month old bug that would cause substitution items to not be shown on the personal plan if they contained a question mark (e.g. 'INF7?inf999')

This commit is contained in:
Deniz Düzgören
2019-10-23 21:15:12 +02:00
parent 1a4b8ce142
commit 10033d999f
24 changed files with 658 additions and 626 deletions
+6 -2
View File
@@ -10,8 +10,8 @@ android {
applicationId "com.denizd.substitutionplan" applicationId "com.denizd.substitutionplan"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode 42 versionCode 43
versionName "2.3.4" versionName "2.4.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
@@ -48,6 +48,10 @@ dependencies {
def room_version = "2.1.0-alpha04" def room_version = "2.1.0-alpha04"
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-beta01'
} }
repositories { repositories {
@@ -2,7 +2,6 @@ package com.denizd.substitutionplan.adapters
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.text.SpannableString import android.text.SpannableString
import android.text.style.StrikethroughSpan import android.text.style.StrikethroughSpan
@@ -26,9 +25,9 @@ import java.util.*
* PlanFragment.kt and its subclasses * PlanFragment.kt and its subclasses
* *
* @param substitutions a list of all substitutions of data type Substitution * @param substitutions a list of all substitutions of data type Substitution
* @param prefs a reference to the app's Shared Preferences used to get user-set colours * @param colours a map of all user-defined colours for the individual courses
*/ */
internal class SubstitutionAdapter(private var substitutions: List<Substitution>, private val prefs: SharedPreferences) : RecyclerView.Adapter<SubstitutionAdapter.CardViewHolder>() { internal class SubstitutionAdapter(private var substitutions: List<Substitution>, private val colours: Map<String, String>) : RecyclerView.Adapter<SubstitutionAdapter.CardViewHolder>() {
/** /**
* The ViewHolder class used by SubstitutionAdapter.kt to resolve references to views in * The ViewHolder class used by SubstitutionAdapter.kt to resolve references to views in
@@ -114,11 +113,7 @@ internal class SubstitutionAdapter(private var substitutions: List<Substitution>
View.GONE View.GONE
} else { } else {
val colourString = SubstUtil.getColourString(holder.course.text.toString()) val colourString = SubstUtil.getColourString(holder.course.text.toString())
val colourPrefsInt = if (colourString.isNotEmpty()) { val colourPrefsInt = colours[colourString] ?: ""
prefs.getString("card$colourString", "") ?: ""
} else {
""
}
colour = SubstUtil.getColourForString(colourPrefsInt, holder.context) colour = SubstUtil.getColourForString(colourPrefsInt, holder.context)
cardBackgroundColour = if (colour != 0) { cardBackgroundColour = if (colour != 0) {
colour colour
@@ -0,0 +1,8 @@
package com.denizd.substitutionplan.data
internal enum class Caller {
SUBSTITUTION,
FOODMENU,
JOBSERVICE,
SETTINGS
}
@@ -1,279 +0,0 @@
package com.denizd.substitutionplan.data
import android.app.*
import android.content.Context
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import android.os.AsyncTask
import android.os.Build
import android.util.Log
import androidx.preference.PreferenceManager
import android.view.View
import android.widget.RemoteViews
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.denizd.substitutionplan.database.FoodRepository
import com.denizd.substitutionplan.R
import com.denizd.substitutionplan.database.SubstRepository
import com.denizd.substitutionplan.activities.Main
import com.denizd.substitutionplan.models.Food
import com.denizd.substitutionplan.models.Substitution
import com.google.android.material.snackbar.Snackbar
import org.jsoup.Jsoup
import java.lang.ref.WeakReference
import kotlin.collections.ArrayList
/**
* This class gets data from the djd4rkn355.github.io domain asynchronously and persists it in the
* database. By using booleans as parameters, network usage and speed can be improved as only
* requested data will be downloaded and processed. Furthermore, the class provides a boolean
* to determine whether to send a notification to the user, if applicable.
*
* @param isPlan substitution plan will be downloaded and persisted in the database if true
* @param isMenu food menu will be downloaded and persisted in the database if true
* @param isJobService enables sending of a notification if true
* @param forced if true, times for food menu and substitution table will be overwritten
* with empty values to enable a forced refresh
* @param context the context of the application that will be stored as a WeakReference
* to avoid memory leakage
* @param application a reference to the application that will be stored as a WeakReference
* @param parentView a reference to the parent view that will be stored as a WeakReference
*/
internal class DataFetcher(
isPlan: Boolean,
isMenu: Boolean,
isJobService: Boolean = false,
forced: Boolean = false,
context: Context,
application: Application,
parentView: View?
) : AsyncTask<Void, Void, Void>() {
private var jobService = isJobService
private var plan = isPlan
private var menu = isMenu
private var forceRefresh = forced
private var mContext = WeakReference(context)
private var mApplication = application
private var mView = WeakReference(parentView)
private var notificationText = ""
private var informational = ""
private val prefs = PreferenceManager.getDefaultSharedPreferences(mContext.get())
private val edit = prefs.edit()
private var currentTime = ""
private var currentFoodTime = ""
private var substUrl = "https://djd4rkn355.github.io/avh_substitutions.html"
private var foodUrl = "https://djd4rkn355.github.io/food.html"
private var websitePriority = 0
override fun doInBackground(vararg params: Void?): Void? {
try {
if (prefs.getBoolean("testUrls", false)) {
substUrl = "https://djd4rkn355.github.io/subst_test.html"
foodUrl = "https://djd4rkn355.github.io/food_test.html"
} else if ((prefs.getString("custom_test_url", "") ?: "").isNotEmpty()) {
substUrl = "https://djd4rkn355.github.io/${prefs.getString("custom_test_url", "")}"
}
if (forceRefresh) {
edit.putString("timeNew", "").putString("newFoodTime", "").apply()
}
if (menu) {
requestFoodMenuData()
}
if (plan) {
requestSubstPlan()
}
if (plan || menu) {
mView.get()?.let { v: View ->
var snackText = "${mContext.get()?.getText(R.string.last_updated)} ${when {
plan -> currentTime
menu -> currentFoodTime
else -> "---"
}}"
snackText = if (forceRefresh) mContext.get()?.getString(R.string.force_refresh_successful) ?: "" else snackText
val snackBarView = v.findViewById<View>(R.id.coordination)
Snackbar.make(snackBarView, snackText, Snackbar.LENGTH_LONG).show()
}
}
} catch (e: Exception) {
mView.get()?.let { v: View ->
val snackBarView = v.findViewById<View>(R.id.coordination)
Snackbar.make(snackBarView, mContext.get()?.getString(R.string.no_internet_connection) ?: "", Snackbar.LENGTH_LONG).setBackgroundTint(ContextCompat.getColor(mContext.get()!!,
R.color.colorError
)).show()
}
prefs.edit().putString("debug_recent_exception", Log.getStackTraceString(e)).apply()
}
return null
}
override fun onPostExecute(result: Void?) {
mView.get()?.let {
try {
it.findViewById<SwipeRefreshLayout>(R.id.swipe_refresh_layout).isRefreshing = false
} catch (ignored: Exception) {
}
}
mView.clear()
mContext.clear()
}
private fun requestFoodMenuData() {
val docFood = Jsoup.connect(foodUrl).get()
currentFoodTime = docFood.select("h1")[0].text()
val foodRepository = FoodRepository(mApplication)
if (currentFoodTime != prefs.getString("newFoodTime", "")) {
val foodElements = docFood.select("th")
foodRepository.deleteAll()
val indices = ArrayList<Int>()
val daysAndVon = arrayOf("montag", "dienstag", "mittwoch", "donnerstag", "freitag", "von", "wünschen")
for (i in 0 until foodElements.size) {
if (SubstUtil.checkStringForArray(foodElements[i].text(), daysAndVon, true)) indices.add(i)
}
indices.add(foodElements.size)
for (l in 0 until indices.size - 1) {
var s = ""
for (i2 in indices[l] until indices[l + 1]) {
if (s.isEmpty()) {
s = foodElements[i2].text()
} else {
s += "\n${foodElements[i2].text()}"
}
}
foodRepository.insert(Food(s, l))
}
edit.putString("newFoodTime", currentFoodTime).apply()
}
}
private fun requestSubstPlan() {
val doc = Jsoup.connect(substUrl).get()
currentTime = doc.select("h1")[0].text()
val substRepo = SubstRepository(mApplication)
if (currentTime != prefs.getString("timeNew", "")) {
val rows = doc.select("tr")
val paragraphs = doc.select("h6")
val substArray = ArrayList<Substitution>()
val coursePreference = prefs.getString("courses", "") ?: ""
val classPreference = prefs.getString("classes", "") ?: ""
for (i in 0 until paragraphs.size) {
if (i == 0) {
informational = formatElement(paragraphs[i].html())
} else {
informational += "\n\n" + formatElement(paragraphs[i].html())
}
}
edit.putString("informational", informational).apply()
substRepo.deleteAllSubst()
for (i in 0 until rows.size) {
val row = rows[i]
val cols = row.select("th")
val group = cols[0].text()
val date = cols[1].text()
val subst = Substitution(
group = group,
date = date,
time = cols[2].text(),
course = cols[3].text(),
room = cols[4].text(),
additional = cols[5].text(),
teacher = cols[6].text(),
type = cols[7].text(),
priority = SubstUtil.assignRanking(group, (date.length > 2 && date.substring(0, 3) == "psa")),
date_priority = SubstUtil.assignDatePriority(date),
website_priority = websitePriority
)
substArray.add(subst)
substRepo.insert(subst)
websitePriority += 1
}
var countOfNotificationItems = 0
var countOfMoreNotificationItems = 0
if (jobService && prefs.getBoolean("notif", true)) {
substArray.filter { substItem ->
SubstUtil.checkPersonalSubstitutions(
substItem,
coursePreference,
classPreference,
false
)
}.forEach { substItem ->
if (countOfNotificationItems < 4) {
notificationText += ("${if (notificationText.isNotEmpty()) ",\n" else ""}${substItem.course}: ${if (substItem.additional.isNotEmpty()) substItem.additional else if (substItem.type.isNotEmpty()) substItem.type else "---"}")
countOfNotificationItems += 1
} else {
countOfMoreNotificationItems += 1
}
}
if (countOfMoreNotificationItems > 0) {
notificationText += mContext.get()?.resources?.getQuantityString(R.plurals.notification_more_messages, countOfMoreNotificationItems, countOfMoreNotificationItems)
}
}
if (notificationText.isNotEmpty()) {
sendNotification()
}
edit.putString("timeNew", currentTime).apply()
}
}
private fun sendNotification() {
mContext.get()?.let { context ->
val openApp = Intent(context, Main::class.java)
openApp.flags = Intent.FLAG_ACTIVITY_NEW_TASK
openApp.flags += Intent.FLAG_ACTIVITY_CLEAR_TASK
val openAppPending = PendingIntent.getActivity(context, 0, openApp, 0)
val notificationLayout = RemoteViews(context.packageName, R.layout.notification)
notificationLayout.setTextViewText(R.id.notification_title, context.getString(
R.string.substitution_plan
))
notificationLayout.setTextViewText(R.id.notification_textview, notificationText)
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
SubstUtil.getNotificationChannel(context, prefs)
}
val notification = NotificationCompat.Builder(context, SubstUtil.notificationChannelId)
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
.setCustomContentView(notificationLayout)
.setContentIntent(openAppPending)
.setAutoCancel(true)
.setColor(ContextCompat.getColor(context, R.color.colorAccent))
notification.setSmallIcon(if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) R.drawable.ic_avh else R.drawable.ic_avhlogo)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
val sound = if ((prefs.getString("ringtoneUri", "") ?: "").isNotEmpty()) {
Uri.parse(prefs.getString("ringtoneUri", "") ?: "")
} else {
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
}
notification.setSound(sound)
}
manager.notify(1, notification.build())
}
}
private fun formatElement(element: String): String {
return element
.replace("<br>", "\n")
}
}
@@ -81,16 +81,11 @@ internal object SubstUtil {
const val notificationChannelId = "general" const val notificationChannelId = "general"
fun getEmptyGeneralSubstitution(context: Context) = arrayOf( // Constants for Firebase Cloud Messaging Topics
Substitution("", "", "", context.getString(R.string.plan_empty), "", "", "", "", 0, 0, 0) const val FB_TOPIC_ANDROID = "substitutions-android"
).toList() const val FB_TOPIC_BROADCAST = "substitutions-broadcast"
const val FB_TOPIC_IOS = "substitutions-ios"
fun getEmptyPersonalSubstitution(context: Context) = const val FB_TOPIC_DEVELOPMENT = "substitutions-debug"
Substitution("", "", "", context.getString(R.string.personal_plan_empty), "", "", "", "", 0, 0, 0)
fun getEmptyFoodMenu(context: Context) = arrayOf(
Food("\n${context.getString(R.string.food_menu_empty)}\n", 0)
).toList()
/** /**
* This function returns an array that contains all data relevant to tinting individual entries * This function returns an array that contains all data relevant to tinting individual entries
@@ -248,39 +243,6 @@ internal object SubstUtil {
edit.apply() edit.apply()
} }
/**
* This function serves for checking whether a course on the substitution plan is relevant to
* the user and should be shown on their personal plan as well as in their notifications
*
* @param substitution the substitution item
* @param coursePreference a string that represents the courses the user is enrolled in
* @param classPreference a string that represents the group the user is in
* @param psa decide whether or not any PSA items should be included in the filter
*
* @return true if the substitution is relevant to the user, false otherwise
*/
fun checkPersonalSubstitutions(substitution: Substitution, coursePreference: String, classPreference: String, psa: Boolean): Boolean {
val group = substitution.group
val course = substitution.course
if (psa && substitution.date.isNotEmpty() && substitution.date.substring(0, 3) == "psa") {
return true
}
if (coursePreference.isEmpty() && classPreference.isNotEmpty()) {
if (group.isNotEmpty()) {
if (classPreference.contains(group) || group.contains(classPreference)) {
return true
}
}
} else if (classPreference.isNotEmpty() && coursePreference.isNotEmpty()) {
if (group != "" && course != "") {
if ((classPreference.contains(group) || group.contains(classPreference)) && coursePreference.contains(course)) {
return true
}
}
}
return false
}
/** /**
* This function is used throughout some classes to set the theme according to the device's * This function is used throughout some classes to set the theme according to the device's
* Android version before an app theme may be picked in the settings. As Main.kt as well as * Android version before an app theme may be picked in the settings. As Main.kt as well as
@@ -1,12 +0,0 @@
package com.denizd.substitutionplan.data
/**
* Enum class for declaring Firebase Cloud Messaging topics without using the raw strings
* throughout the project to avoid human error
*/
internal enum class Topic(val tag: String) {
ANDROID("substitutions-android"),
BROADCAST("substitutions-broadcast"),
IOS("substitutions-ios"),
DEVELOPMENT("substitutions-debug")
}
@@ -1,27 +0,0 @@
package com.denizd.substitutionplan.database
import android.app.Application
import android.os.AsyncTask
import androidx.lifecycle.LiveData
import com.denizd.substitutionplan.models.Food
internal class FoodRepository(application: Application) {
private val substDao: SubstDao?
val allFoods: LiveData<List<Food>>?
init {
val database =
SubstDatabase.getFoodInstance(application)
substDao = database?.substDao()
allFoods = substDao?.allFoods
}
fun insert(food: Food) {
substDao?.insertFood(food)
}
fun deleteAll() {
substDao?.deleteAllFoods()
}
}
@@ -1,32 +0,0 @@
package com.denizd.substitutionplan.database
import android.app.Application
import android.view.View
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.denizd.substitutionplan.data.DataFetcher
import com.denizd.substitutionplan.models.Food
internal class FoodViewModel(application: Application) : AndroidViewModel(application) {
private val repository: FoodRepository = FoodRepository(application)
val allFoods: LiveData<List<Food>>?
private val app = application
init {
allFoods = repository.allFoods
}
fun refresh(swipeRefreshLayout: SwipeRefreshLayout, rootView: View) {
swipeRefreshLayout.isRefreshing = true
DataFetcher(
isPlan = false,
isMenu = true,
isJobService = false,
context = app,
application = app,
parentView = rootView
).execute()
}
}
@@ -0,0 +1,31 @@
package com.denizd.substitutionplan.database
import android.app.Application
import android.database.Cursor
import android.media.RingtoneManager
import com.denizd.substitutionplan.models.Ringtone
internal class SettingsRepository(private val application: Application) {
val ringtones: List<Ringtone> by lazy {
lateinit var ringtoneCursor: Cursor
val ringtoneManager = RingtoneManager(application).apply {
setType(RingtoneManager.TYPE_NOTIFICATION)
ringtoneCursor = cursor
}
val alarms = ArrayList<Ringtone>()
while (!ringtoneCursor.isAfterLast && ringtoneCursor.moveToNext()) {
val position = ringtoneCursor.position
alarms.add(
Ringtone(
ringtoneManager.getRingtone(position).getTitle(application),
ringtoneManager.getRingtoneUri(position).toString()
)
)
}
alarms
}
}
@@ -37,12 +37,12 @@ internal abstract class SubstDatabase : RoomDatabase() {
} }
} }
private var instance: SubstDatabase? = null private var substInstance: SubstDatabase? = null
fun getInstance(context: Context): SubstDatabase? { fun getSubstInstance(context: Context): SubstDatabase? {
if (instance == null) { if (substInstance == null) {
synchronized (SubstDatabase::class) { synchronized (SubstDatabase::class) {
instance = Room.databaseBuilder(context.applicationContext, substInstance = Room.databaseBuilder(context.applicationContext,
SubstDatabase::class.java, SubstDatabase::class.java,
"subst_database") "subst_database")
.addMigrations(addTeacherColumn, addTypeColumn, addDatePriorityColumn, addWebsitePriorityColumn) .addMigrations(addTeacherColumn, addTypeColumn, addDatePriorityColumn, addWebsitePriorityColumn)
@@ -50,7 +50,7 @@ internal abstract class SubstDatabase : RoomDatabase() {
.build() .build()
} }
} }
return instance return substInstance
} }
private var foodInstance: SubstDatabase? = null private var foodInstance: SubstDatabase? = null
@@ -1,30 +1,379 @@
package com.denizd.substitutionplan.database package com.denizd.substitutionplan.database
import android.app.Application import android.app.Application
import android.os.AsyncTask import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import android.util.Log
import android.widget.RemoteViews
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.preference.PreferenceManager
import com.denizd.substitutionplan.R
import com.denizd.substitutionplan.activities.Main
import com.denizd.substitutionplan.data.Caller
import com.denizd.substitutionplan.data.SubstUtil
import com.denizd.substitutionplan.models.Food
import com.denizd.substitutionplan.models.Substitution import com.denizd.substitutionplan.models.Substitution
import org.jsoup.Jsoup
internal class SubstRepository(application: Application) { internal class SubstRepository(private val application: Application) {
private val substDao: SubstDao? private val substitutionDao: SubstDao?
private val foodMenuDao: SubstDao?
val allSubstitutionsSorted: LiveData<List<Substitution>>? val allSubstitutionsSorted: LiveData<List<Substitution>>?
val allSubstitutionsOriginal: LiveData<List<Substitution>>? val allSubstitutionsOriginal: LiveData<List<Substitution>>?
val allFoodItems: LiveData<List<Food>>?
private val prefs = PreferenceManager.getDefaultSharedPreferences(application)
val substitutionPlanColours: Map<String, String>
get() = mapOf(
"German" to prefs.getNonNullString("cardGerman"),
"Maths" to prefs.getNonNullString("cardMaths"),
"English" to prefs.getNonNullString("cardEnglish"),
"PhysEd" to prefs.getNonNullString("cardPhysEd"),
"Politics" to prefs.getNonNullString("cardPolitics"),
"Theatre" to prefs.getNonNullString("cardTheatre"),
"Physics" to prefs.getNonNullString("cardPhysics"),
"Biology" to prefs.getNonNullString("cardBiology"),
"Chemistry" to prefs.getNonNullString("cardChemistry"),
"Philosophy" to prefs.getNonNullString("cardPhilosophy"),
"Latin" to prefs.getNonNullString("cardLatin"),
"Spanish" to prefs.getNonNullString("cardSpanish"),
"French" to prefs.getNonNullString("cardFrench"),
"CompSci" to prefs.getNonNullString("cardCompSci"),
"History" to prefs.getNonNullString("cardHistory"),
"Religion" to prefs.getNonNullString("cardReligion"),
"Geography" to prefs.getNonNullString("cardGeography"),
"Arts" to prefs.getNonNullString("cardArts"),
"Music" to prefs.getNonNullString("cardMusic"),
"Turkish" to prefs.getNonNullString("cardTurkish"),
"Chinese" to prefs.getNonNullString("cardChinese"),
"GLL" to prefs.getNonNullString("cardGLL"),
"WAT" to prefs.getNonNullString("cardWAT"),
"Forder" to prefs.getNonNullString("cardForder"),
"WP" to prefs.getNonNullString("cardWP")
)
val shouldAutoRefresh: Boolean
get() = prefs.getBoolean("autoRefresh", false)
val shouldUseAppSorting: Boolean
get() = prefs.getBoolean("app_specific_sorting", true)
val emptyGeneralPlan: List<Substitution> by lazy {
listOf(Substitution("", "", "", application.getString(R.string.plan_empty), "", "", "", "", 0, 0, 0))
}
val emptyPersonalSubstitution: Substitution by lazy {
Substitution("", "", "", application.getString(R.string.personal_plan_empty), "", "", "", "", 0, 0, 0)
}
val emptyFoodMenu: List<Food> by lazy {
listOf(Food("\n${application.getString(R.string.food_menu_empty)}\n", 0))
}
init { init {
val database = val substInstance = SubstDatabase.getSubstInstance(application)
SubstDatabase.getInstance(application) val foodInstance = SubstDatabase.getFoodInstance(application)
substDao = database?.substDao() substitutionDao = substInstance?.substDao()
allSubstitutionsSorted = substDao?.allSubstitutionsSorted foodMenuDao = foodInstance?.substDao()
allSubstitutionsOriginal = substDao?.allSubstitutionsOriginal allSubstitutionsSorted = substitutionDao?.allSubstitutionsSorted
allSubstitutionsOriginal = substitutionDao?.allSubstitutionsOriginal
allFoodItems = foodMenuDao?.allFoods
} }
fun insert(substitution: Substitution) { private fun insert(substitution: Substitution) {
substDao?.insertSubst(substitution) substitutionDao?.insertSubst(substitution)
} }
fun deleteAllSubst() { private fun insert(food: Food) {
substDao?.deleteAllSubst() foodMenuDao?.insertFood(food)
}
private fun deleteAllSubst() {
substitutionDao?.deleteAllSubst()
}
private fun deleteAllFoodItems() {
foodMenuDao?.deleteAllFoods()
}
fun getGridColumnCount(config: Configuration): Int {
return if (application.resources.getBoolean(R.bool.isTablet)) {
when (config.orientation) {
Configuration.ORIENTATION_PORTRAIT -> 2
else -> 3
}
} else {
when (config.orientation) {
Configuration.ORIENTATION_PORTRAIT -> 1
else -> 2
}
}
}
private fun SharedPreferences.getNonNullString(key: String): String {
return getString(key, "") ?: ""
}
fun externalNonNullString(key: String): String {
return prefs.getNonNullString(key)
}
private fun SharedPreferences.putAndApplyString(key: String, value: String) {
edit().putString(key, value).apply()
}
fun putAndApplyString(key: String, value: String) {
prefs.putAndApplyString(key, value)
}
private fun Substitution.checkIfPersonal(classPreference: String, coursePreference: String, includesPsa: Boolean = false): Boolean {
if (includesPsa && date.isNotEmpty() && date.substring(0, 3) == "psa") {
return true
}
if (coursePreference.isEmpty() && classPreference.isNotEmpty()) {
if (group.isNotEmpty()) {
if (classPreference.contains(group) || group.contains(classPreference)) {
return true
}
}
} else if (classPreference.isNotEmpty() && coursePreference.isNotEmpty()) {
if (group != "" && course != "") {
if ((classPreference.contains(group) || group.contains(classPreference))) {
val questionMark = course.indexOf("?")
if (questionMark == -1) {
if (coursePreference.contains(course)) {
return true
}
} else {
if (coursePreference.contains(course.substring(0, questionMark - 1))
|| coursePreference.contains(course.substring(questionMark + 1))) {
return true
}
}
}
}
}
return false
}
fun checkIfSubstitutionPersonal(substitution: Substitution, classPreference: String, coursePreference: String, includesPsa: Boolean): Boolean {
return substitution.checkIfPersonal(classPreference, coursePreference, includesPsa)
}
private fun getData(caller: Caller): Pair<String, Boolean> {
fun requestSubstPlan(url: String): Pair<String, Boolean> {
var notificationText = ""
var hasUpdated = false
val doc = Jsoup.connect(url).get()
val currentTime = doc.select("h1")[0].text()
if (currentTime != prefs.getNonNullString("timeNew")) {
val rows = doc.select("tr")
val paragraphs = doc.select("h6")
val substArray = ArrayList<Substitution>()
val coursePreference = prefs.getNonNullString("courses")
val classPreference = prefs.getNonNullString("classes")
var informational = ""
for (i in 0 until paragraphs.size) {
if (i == 0) {
informational = paragraphs[i].html().replace("<br>", "\n")
} else {
informational += "\n\n" + paragraphs[i].html().replace("<br>", "\n")
}
}
prefs.putAndApplyString("informational", informational)
deleteAllSubst()
for (i in 0 until rows.size) {
val row = rows[i]
val cols = row.select("th")
val group = cols[0].text()
val date = cols[1].text()
val subst = Substitution(
group = group,
date = date,
time = cols[2].text(),
course = cols[3].text(),
room = cols[4].text(),
additional = cols[5].text(),
teacher = cols[6].text(),
type = cols[7].text(),
priority = SubstUtil.assignRanking(group, (date.length > 2 && date.substring(0, 3) == "psa")), // TODO make fancier pls
date_priority = SubstUtil.assignDatePriority(date),
website_priority = i
)
if (caller == Caller.JOBSERVICE && prefs.getBoolean("notif", true)) {
if (subst.checkIfPersonal(classPreference, coursePreference)) {
substArray.add(subst)
}
}
insert(subst)
}
var countOfNotificationItems = 0
var countOfMoreNotificationItems = 0
if (caller == Caller.JOBSERVICE && prefs.getBoolean("notif", true)) {
substArray.forEach { substItem ->
if (countOfNotificationItems < 4) {
notificationText += ("${
if (notificationText.isNotEmpty()) ",\n" else ""
}${substItem.course}: ${
if (substItem.additional.isNotEmpty()) substItem.additional else if (substItem.type.isNotEmpty()) substItem.type else "---"
}")
countOfNotificationItems += 1
} else {
countOfMoreNotificationItems += 1
}
}
if (countOfMoreNotificationItems > 0) {
notificationText += application.resources.getQuantityString(R.plurals.notification_more_messages, countOfMoreNotificationItems, countOfMoreNotificationItems)
}
}
prefs.putAndApplyString("timeNew", currentTime)
hasUpdated = true
}
return Pair(notificationText, hasUpdated)
}
fun requestFoodMenuData(url: String): Boolean {
val docFood = Jsoup.connect(url).get()
val currentFoodTime = docFood.select("h1")[0].text()
return if (currentFoodTime != prefs.getString("newFoodTime", "")) {
val foodElements = docFood.select("th")
deleteAllFoodItems()
val indices = ArrayList<Int>()
val daysAndVon = arrayOf("montag", "dienstag", "mittwoch", "donnerstag", "freitag", "von", "wünschen")
for (i in 0 until foodElements.size) {
if (SubstUtil.checkStringForArray(foodElements[i].text(), daysAndVon, true)) indices.add(i)
}
indices.add(foodElements.size)
for (l in 0 until indices.size - 1) {
var s = ""
for (i2 in indices[l] until indices[l + 1]) {
if (s.isEmpty()) {
s = foodElements[i2].text()
} else {
s += "\n${foodElements[i2].text()}"
}
}
insert(Food(s, l))
}
prefs.putAndApplyString("newFoodTime", currentFoodTime)
true
} else {
false
}
}
fun sendNotification(notificationText: String) {
val openApp = Intent(application, Main::class.java)
openApp.flags = Intent.FLAG_ACTIVITY_NEW_TASK
openApp.flags += Intent.FLAG_ACTIVITY_CLEAR_TASK
val openAppPending = PendingIntent.getActivity(application, 0, openApp, 0)
val notificationLayout = RemoteViews(application.packageName, R.layout.notification)
notificationLayout.setTextViewText(R.id.notification_title, application.getString(
R.string.substitution_plan
))
notificationLayout.setTextViewText(R.id.notification_textview, notificationText)
val manager = application.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
SubstUtil.getNotificationChannel(application, prefs)
}
val notification = NotificationCompat.Builder(application, SubstUtil.notificationChannelId)
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
.setCustomContentView(notificationLayout)
.setContentIntent(openAppPending)
.setAutoCancel(true)
.setColor(ContextCompat.getColor(application, R.color.colorAccent))
.setSmallIcon(
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) R.drawable.ic_avh else R.drawable.ic_avhlogo
)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
val sound = if (prefs.getNonNullString("ringtoneUri").isNotEmpty()) {
Uri.parse(prefs.getNonNullString("ringtoneUri"))
} else {
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
}
notification.setSound(sound)
}
manager.notify(1, notification.build())
}
val urlPair = when {
prefs.getBoolean("testUrls", false) -> Pair(
"https://djd4rkn355.github.io/subst_test.html",
"https://djd4rkn355.github.io/food_test.html"
)
prefs.getNonNullString("custom_test_url").isNotEmpty() -> Pair(
"https://djd4rkn355.github.io/${prefs.getString("custom_test_url", "")}",
"https://djd4rkn355.github.io/food.html"
)
else -> Pair(
"https://djd4rkn355.github.io/avh_substitutions.html",
"https://djd4rkn355.github.io/food.html"
)
}
var error = false
val returnText = try {
val foodMenuHasUpdated = requestFoodMenuData(urlPair.second)
val planResult = requestSubstPlan(urlPair.first)
val notificationText = planResult.first
val planHasUpdated = planResult.second
if (caller == Caller.JOBSERVICE && notificationText.isNotEmpty()) {
sendNotification(notificationText)
}
when (caller) {
Caller.SUBSTITUTION -> {
if (planHasUpdated) {
application.getString(R.string.plan_updated)
} else {
"${application.getString(R.string.last_updated)} ${prefs.getNonNullString("timeNew")}"
}
}
Caller.FOODMENU -> {
if (foodMenuHasUpdated) {
application.getString(R.string.food_menu_updated)
} else {
"${application.getString(R.string.last_updated)} ${prefs.getNonNullString("newFoodTime")}"
}
}
Caller.SETTINGS -> application.getString(R.string.force_refresh_successful)
else -> ""
}
} catch (e: Exception) {
error = true
prefs.putAndApplyString("debug_recent_exception", Log.getStackTraceString(e))
application.getString(R.string.an_error_occurred)
}
return Pair(returnText, error)
}
fun fetchDataOnline(caller: Caller): Pair<String, Boolean> {
return getData(caller)
} }
} }
@@ -1,34 +0,0 @@
package com.denizd.substitutionplan.database
import android.app.Application
import android.view.View
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.denizd.substitutionplan.data.DataFetcher
import com.denizd.substitutionplan.models.Substitution
internal class SubstViewModel(application: Application) : AndroidViewModel(application) {
private val repository: SubstRepository = SubstRepository(application)
val allSubstitutionsSorted: LiveData<List<Substitution>>?
val allSubstitutionsOriginal: LiveData<List<Substitution>>?
private val app = application
init {
allSubstitutionsSorted = repository.allSubstitutionsSorted
allSubstitutionsOriginal = repository.allSubstitutionsOriginal
}
fun refresh(swipeRefreshLayout: SwipeRefreshLayout, rootView: View, refreshMenu: Boolean) {
swipeRefreshLayout.isRefreshing = true
DataFetcher(
isPlan = true,
isMenu = refreshMenu,
isJobService = false,
context = app,
application = app,
parentView = rootView
).execute()
}
}
@@ -1,36 +1,29 @@
package com.denizd.substitutionplan.fragments package com.denizd.substitutionplan.fragments
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import androidx.preference.PreferenceManager
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.denizd.substitutionplan.R
import com.denizd.substitutionplan.adapters.FoodAdapter import com.denizd.substitutionplan.adapters.FoodAdapter
import com.denizd.substitutionplan.data.SubstUtil import com.denizd.substitutionplan.viewmodels.FoodViewModel
import com.denizd.substitutionplan.database.FoodViewModel
import com.denizd.substitutionplan.databinding.FoodLayoutBinding import com.denizd.substitutionplan.databinding.FoodLayoutBinding
import com.denizd.substitutionplan.models.Food import com.denizd.substitutionplan.models.Food
import com.google.android.material.snackbar.Snackbar
internal class FoodFragment : Fragment() { internal class FoodFragment : Fragment() {
private val mAdapter = FoodAdapter(ArrayList()) private val mAdapter = FoodAdapter(ArrayList())
private lateinit var mContext: Context private lateinit var viewModel: FoodViewModel
private lateinit var prefs: SharedPreferences
private lateinit var foodViewModel: FoodViewModel
private lateinit var binding: FoodLayoutBinding private lateinit var binding: FoodLayoutBinding
private lateinit var snackBarContainer: CoordinatorLayout
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
prefs = PreferenceManager.getDefaultSharedPreferences(mContext)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FoodLayoutBinding.inflate(inflater, container, false) binding = FoodLayoutBinding.inflate(inflater, container, false)
@@ -40,28 +33,41 @@ internal class FoodFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
snackBarContainer = view.rootView.findViewById(R.id.coordination) // TODO replace findViewById with ViewBinding
binding.recyclerView.apply { binding.recyclerView.apply {
hasFixedSize() hasFixedSize()
layoutManager = GridLayoutManager(mContext, 1) layoutManager = GridLayoutManager(requireContext(), 1)
adapter = mAdapter adapter = mAdapter
} }
foodViewModel = ViewModelProviders.of(this).get(FoodViewModel::class.java) viewModel = ViewModelProviders.of(this).get(FoodViewModel::class.java)
foodViewModel.allFoods?.observe(this, Observer<List<Food>> { foodList -> viewModel.allFoodItems?.observe(this, Observer<List<Food>> { foodList ->
binding.recyclerView.scheduleLayoutAnimation() binding.recyclerView.scheduleLayoutAnimation()
mAdapter.setFood(if (foodList.isEmpty()) { mAdapter.setFood(if (foodList.isEmpty()) {
SubstUtil.getEmptyFoodMenu(mContext) viewModel.emptyFoodMenu
} else { } else {
foodList foodList
}) })
}) })
if (prefs.getBoolean("autoRefresh", false)) { if (viewModel.shouldAutoRefresh) {
foodViewModel.refresh(swipeRefreshLayout = binding.swipeRefreshLayout, rootView = view.rootView) refreshAndDisplaySnackBar()
} }
binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
foodViewModel.refresh(swipeRefreshLayout = binding.swipeRefreshLayout, rootView = view.rootView) refreshAndDisplaySnackBar()
}
}
private fun refreshAndDisplaySnackBar() {
viewModel.refresh { result, error ->
val snackBar = Snackbar.make(snackBarContainer, result, Snackbar.LENGTH_LONG)
if (error) {
snackBar.setBackgroundTint(ContextCompat.getColor(requireContext(), R.color.colorError))
}
snackBar.show()
binding.swipeRefreshLayout.isRefreshing = false
} }
} }
} }
@@ -3,7 +3,6 @@ package com.denizd.substitutionplan.fragments
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.denizd.substitutionplan.data.SubstUtil
import com.denizd.substitutionplan.models.Substitution import com.denizd.substitutionplan.models.Substitution
internal class GeneralPlanFragment : PlanFragment() { internal class GeneralPlanFragment : PlanFragment() {
@@ -13,7 +12,7 @@ internal class GeneralPlanFragment : PlanFragment() {
substitutionPlan?.observe(this, Observer<List<Substitution>> { substitutions -> substitutionPlan?.observe(this, Observer<List<Substitution>> { substitutions ->
mAdapter.setSubst(if (substitutions.isEmpty()) { mAdapter.setSubst(if (substitutions.isEmpty()) {
SubstUtil.getEmptyGeneralSubstitution(mContext) viewModel.emptyGeneralPlan
} else { } else {
substitutions substitutions
}) })
@@ -3,7 +3,6 @@ package com.denizd.substitutionplan.fragments
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.denizd.substitutionplan.data.SubstUtil
import com.denizd.substitutionplan.models.Substitution import com.denizd.substitutionplan.models.Substitution
internal class PersonalPlanFragment : PlanFragment() { internal class PersonalPlanFragment : PlanFragment() {
@@ -11,30 +10,28 @@ internal class PersonalPlanFragment : PlanFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val coursePreference = prefs.getString("courses", "") ?: "" val coursePreference = viewModel.getNonNullString("courses")
val classPreference = prefs.getString("classes", "") ?: "" val classPreference = viewModel.getNonNullString("classes")
substitutionPlan?.observe(this, Observer<List<Substitution>> { substitutions -> substitutionPlan?.observe(this, Observer<List<Substitution>> { substitutions ->
planCardList.clear() val list = ArrayList<Substitution>()
isPersonalPlanEmpty = true
binding.recyclerView.visibility = View.VISIBLE
substitutions.filter { substItem -> substitutions.filter { substItem ->
SubstUtil.checkPersonalSubstitutions( viewModel.checkIfSubstitutionPersonal(
substItem, substItem,
coursePreference,
classPreference, classPreference,
coursePreference,
true true
) )
}.forEach { substItem -> }.forEach { substItem ->
planCardList.add(substItem) list.add(substItem)
} }
isPersonalPlanEmpty = (planCardList.size == 1 && planCardList[0].date.substring(0, 3) == "psa") || planCardList.isEmpty() val isPersonalPlanEmpty = (list.size == 1 && list[0].date.substring(0, 3) == "psa") || list.isEmpty()
if (isPersonalPlanEmpty) { if (isPersonalPlanEmpty) {
planCardList.add(SubstUtil.getEmptyPersonalSubstitution(mContext)) list.add(viewModel.emptyPersonalSubstitution)
} }
binding.recyclerView.scheduleLayoutAnimation() binding.recyclerView.scheduleLayoutAnimation()
mAdapter.setSubst(planCardList) mAdapter.setSubst(list)
}) })
} }
} }
@@ -1,43 +1,33 @@
package com.denizd.substitutionplan.fragments package com.denizd.substitutionplan.fragments
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import androidx.preference.PreferenceManager
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.denizd.substitutionplan.R import com.denizd.substitutionplan.R
import com.denizd.substitutionplan.adapters.SubstitutionAdapter import com.denizd.substitutionplan.adapters.SubstitutionAdapter
import com.denizd.substitutionplan.database.SubstViewModel import com.denizd.substitutionplan.viewmodels.SubstViewModel
import com.denizd.substitutionplan.databinding.PlanBinding import com.denizd.substitutionplan.databinding.PlanBinding
import com.denizd.substitutionplan.models.Substitution import com.denizd.substitutionplan.models.Substitution
import com.google.android.material.snackbar.Snackbar
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
internal open class PlanFragment : Fragment() { internal open class PlanFragment : Fragment() {
internal lateinit var mAdapter: SubstitutionAdapter
internal var planCardList = ArrayList<Substitution>()
private lateinit var substViewModel: SubstViewModel
internal lateinit var mContext: Context
internal lateinit var prefs: SharedPreferences
internal var isPersonalPlanEmpty: Boolean = true internal val mAdapter: SubstitutionAdapter by lazy {
internal val handler = Handler() SubstitutionAdapter(ArrayList(), viewModel.substitutionPlanColours)
internal var substitutionPlan: LiveData<List<Substitution>>? = null
internal lateinit var binding: PlanBinding
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
prefs = PreferenceManager.getDefaultSharedPreferences(mContext)
} }
lateinit var viewModel: SubstViewModel
internal var substitutionPlan: LiveData<List<Substitution>>? = null
internal lateinit var binding: PlanBinding
private lateinit var snackBarContainer: CoordinatorLayout
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = PlanBinding.inflate(inflater, container, false) binding = PlanBinding.inflate(inflater, container, false)
@@ -46,51 +36,50 @@ internal open class PlanFragment : Fragment() {
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
binding.recyclerView.layoutManager = GridLayoutManager(mContext, getGridColumnCount(newConfig)) binding.recyclerView.layoutManager = GridLayoutManager(requireContext(), viewModel.getGridColumnCount(newConfig))
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
substViewModel = ViewModelProviders.of(this).get(SubstViewModel::class.java)
substitutionPlan = if (prefs.getBoolean("app_specific_sorting", true)) { snackBarContainer = view.rootView.findViewById(R.id.coordination) // TODO replace findViewById with ViewBinding
substViewModel.allSubstitutionsSorted
viewModel = ViewModelProviders.of(this).get(SubstViewModel::class.java)
substitutionPlan = if (viewModel.shouldUseAppSorting) {
viewModel.allSubstitutionsSorted
} else { } else {
substViewModel.allSubstitutionsOriginal viewModel.allSubstitutionsOriginal
} }
mAdapter = SubstitutionAdapter(planCardList, prefs)
binding.recyclerView.apply { binding.recyclerView.apply {
hasFixedSize() hasFixedSize()
layoutManager = GridLayoutManager(mContext, getGridColumnCount(resources.configuration)) layoutManager = GridLayoutManager(requireContext(), viewModel.getGridColumnCount(resources.configuration))
adapter = mAdapter adapter = mAdapter
} }
if (prefs.getInt("firstTimeOpening", 0) == 1) { // TODO figure something out
substViewModel.refresh(swipeRefreshLayout = binding.swipeRefreshLayout, rootView = view.rootView, refreshMenu = true) // if (prefs.getInt("firstTimeOpening", 0) == 1) {
prefs.edit().putInt("firstTimeOpening", 2).apply() // refreshAndDisplaySnackBar()
} // prefs.edit().putInt("firstTimeOpening", 2).apply()
// }
if (prefs.getBoolean("autoRefresh", false)) { if (viewModel.shouldAutoRefresh) {
substViewModel.refresh(swipeRefreshLayout = binding.swipeRefreshLayout, rootView = view.rootView, refreshMenu = false) refreshAndDisplaySnackBar()
} }
binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
substViewModel.refresh(swipeRefreshLayout = binding.swipeRefreshLayout, rootView = view.rootView, refreshMenu = false) refreshAndDisplaySnackBar()
} }
} }
private fun getGridColumnCount(config: Configuration): Int { private fun refreshAndDisplaySnackBar() {
val tabletSize = resources.getBoolean(R.bool.isTablet) viewModel.refresh { result, error ->
return if (tabletSize) { val snackBar = Snackbar.make(snackBarContainer, result, Snackbar.LENGTH_LONG)
when (config.orientation) { if (error) {
Configuration.ORIENTATION_PORTRAIT -> 2 snackBar.setBackgroundTint(ContextCompat.getColor(requireContext(), R.color.colorError)) // TODO check if requireContext() is dangerous ;(
else -> 3
}
} else {
when (config.orientation) {
Configuration.ORIENTATION_PORTRAIT -> 1
else -> 2
} }
snackBar.show()
binding.swipeRefreshLayout.isRefreshing = false
} }
} }
} }
@@ -5,8 +5,6 @@ import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.database.Cursor
import android.media.RingtoneManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -17,25 +15,23 @@ import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window
import android.widget.* import android.widget.*
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.denizd.substitutionplan.* import com.denizd.substitutionplan.*
import com.denizd.substitutionplan.adapters.ColourPickerAdapter import com.denizd.substitutionplan.adapters.ColourPickerAdapter
import com.denizd.substitutionplan.adapters.CourseColourAdapter import com.denizd.substitutionplan.adapters.CourseColourAdapter
import com.denizd.substitutionplan.adapters.RingtoneAdapter import com.denizd.substitutionplan.adapters.RingtoneAdapter
import com.denizd.substitutionplan.data.DataFetcher
import com.denizd.substitutionplan.data.SubstUtil import com.denizd.substitutionplan.data.SubstUtil
import com.denizd.substitutionplan.data.Topic
import com.denizd.substitutionplan.databinding.ContentSettingsBinding import com.denizd.substitutionplan.databinding.ContentSettingsBinding
import com.denizd.substitutionplan.models.Colour import com.denizd.substitutionplan.models.Colour
import com.denizd.substitutionplan.models.Ringtone import com.denizd.substitutionplan.viewmodels.SettingsViewModel
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessaging
@@ -48,28 +44,21 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
private lateinit var mContext: Context private lateinit var mContext: Context
private lateinit var prefs: SharedPreferences private lateinit var prefs: SharedPreferences
private val customTabsIntent = CustomTabsIntent.Builder().build() as CustomTabsIntent private val customTabsIntent = CustomTabsIntent.Builder().build() as CustomTabsIntent
private var window: Window? = null private var customiseColoursButtonLongPressed = false
private var longPressed = false
private var versionCount = 0 private var versionCount = 0
private var currentCourseSelectedInColourPicker = "" private var currentCourseSelectedInColourPicker = ""
private val ringtones: List<Ringtone> by lazy {
getRingtones()
}
private lateinit var colourRecycler: RecyclerView private lateinit var colourRecycler: RecyclerView
private lateinit var ringtoneDialog: AlertDialog private lateinit var ringtoneDialog: AlertDialog
private lateinit var colourPickerDialog: AlertDialog private lateinit var colourPickerDialog: AlertDialog
private lateinit var binding: ContentSettingsBinding private lateinit var binding: ContentSettingsBinding
private lateinit var textCustomiseColoursTitle: TextView private lateinit var viewModel: SettingsViewModel
private lateinit var textCustomiseColoursDesc: TextView
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
mContext = context mContext = context
prefs = PreferenceManager.getDefaultSharedPreferences(mContext) prefs = PreferenceManager.getDefaultSharedPreferences(mContext)
window = activity?.window
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@@ -80,12 +69,11 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this)[SettingsViewModel::class.java]
binding.apply { binding.apply {
val thisFragment = this@SettingsFragment val thisFragment = this@SettingsFragment
thisFragment.textCustomiseColoursTitle = textCustomiseColoursTitle
thisFragment.textCustomiseColoursDesc = textCustomiseColoursDesc
// Set text // Set text
setRingtoneText() setRingtoneText()
@@ -174,15 +162,15 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
if (Build.VERSION.SDK_INT in 23..28) { if (Build.VERSION.SDK_INT in 23..28) {
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR activity?.window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
window?.navigationBarColor = ContextCompat.getColor(mContext, R.color.colorBackground) activity?.window?.navigationBarColor = ContextCompat.getColor(mContext, R.color.colorBackground)
} }
} }
2 -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) // only accessible on API 28+ 2 -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) // only accessible on API 28+
else -> { else -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
window?.navigationBarColor = ContextCompat.getColor(mContext, R.color.colorBackground) activity?.window?.navigationBarColor = ContextCompat.getColor(mContext, R.color.colorBackground)
} }
} }
} }
@@ -219,14 +207,14 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
R.id.button_courses_help -> createDialog(getString(R.string.courses_help_dialog_title), getString(R.string.courses_help_dialog_text)) R.id.button_courses_help -> createDialog(getString(R.string.courses_help_dialog_title), getString(R.string.courses_help_dialog_text))
R.id.button_order_help -> createDialog(getString(R.string.ordering_systems_dialog_title), getString(R.string.ordering_systems_dialog_text)) R.id.button_order_help -> createDialog(getString(R.string.ordering_systems_dialog_title), getString(R.string.ordering_systems_dialog_text))
R.id.button_forced_refresh -> { R.id.button_forced_refresh -> {
DataFetcher( // val repo = SubstRepository(activity?.application!!)
isPlan = true, // repo.putAndApplyString("timeNew", "")
isMenu = true, // repo.putAndApplyString("newFoodTime", "")
forced = true, // repo.fetchDataOnline(Caller.SETTINGS)
context = mContext, // TODO needs async
application = activity!!.application, // TODO put in viewModel
parentView = view!!.rootView // TODO create viewModel
).execute() // TODO remove TODOs
} }
R.id.button_visit_website -> { R.id.button_visit_website -> {
try { try {
@@ -250,14 +238,14 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
override fun onLongClick(v: View?): Boolean { override fun onLongClick(v: View?): Boolean {
return when (v?.id) { return when (v?.id) {
R.id.button_customise_colours -> { R.id.button_customise_colours -> {
if (!longPressed) { if (!customiseColoursButtonLongPressed) {
textCustomiseColoursTitle.text = getString(R.string.made_by_deniz) binding.textCustomiseColoursTitle.text = getString(R.string.made_by_deniz)
textCustomiseColoursDesc.text = getString(R.string.thanks_for_using) binding.textCustomiseColoursDesc.text = getString(R.string.thanks_for_using)
} else { } else {
textCustomiseColoursTitle.text = getString(R.string.customise_colours_title) binding.textCustomiseColoursTitle.text = getString(R.string.customise_colours_title)
textCustomiseColoursDesc.text = getString(R.string.customise_colours_desc) binding.textCustomiseColoursDesc.text = getString(R.string.customise_colours_desc)
} }
longPressed = !longPressed customiseColoursButtonLongPressed = !customiseColoursButtonLongPressed
true true
} }
R.id.button_courses_help -> { R.id.button_courses_help -> {
@@ -298,9 +286,9 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
R.id.switch_notifications -> { R.id.switch_notifications -> {
prefs.edit().putBoolean("notif", isChecked).apply() prefs.edit().putBoolean("notif", isChecked).apply()
if (isChecked) { if (isChecked) {
subscribeToTopic(Topic.ANDROID) subscribeToTopic(SubstUtil.FB_TOPIC_ANDROID)
} else { } else {
unsubscribeFromTopic(Topic.ANDROID) unsubscribeFromTopic(SubstUtil.FB_TOPIC_ANDROID)
} }
} }
R.id.switch_personalised_plan -> { R.id.switch_personalised_plan -> {
@@ -389,11 +377,14 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
val titleText = dialogView.findViewById<TextView>(R.id.empty_textviewtitle) val titleText = dialogView.findViewById<TextView>(R.id.empty_textviewtitle)
titleText.text = getString(R.string.pick_ringtone_dialog_title) titleText.text = getString(R.string.pick_ringtone_dialog_title)
val recycler = dialogView.findViewById<RecyclerView>(R.id.recyclerView) dialogView.findViewById<RecyclerView>(R.id.recyclerView).apply {
recycler.apply {
hasFixedSize() hasFixedSize()
layoutManager = GridLayoutManager(mContext, 1) layoutManager = GridLayoutManager(mContext, 1)
adapter = RingtoneAdapter(ringtones, this@SettingsFragment) if (!viewModel.ringtonesInitialised) {
viewModel.getRingtones {
adapter = RingtoneAdapter(viewModel.ringtones, this@SettingsFragment)
}
}
} }
ringtoneDialog = ringtoneCustomiserBuilder.setView(dialogView).create() ringtoneDialog = ringtoneCustomiserBuilder.setView(dialogView).create()
@@ -464,10 +455,10 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
} }
this == "_devchannel" -> { this == "_devchannel" -> {
val subbed = if (prefs.getBoolean("subscribedToFBDebugChannel", false)) { val subbed = if (prefs.getBoolean("subscribedToFBDebugChannel", false)) {
unsubscribeFromTopic(Topic.DEVELOPMENT) unsubscribeFromTopic(SubstUtil.FB_TOPIC_DEVELOPMENT)
"Unsubscribed from" "Unsubscribed from"
} else { } else {
subscribeToTopic(Topic.DEVELOPMENT) subscribeToTopic(SubstUtil.FB_TOPIC_DEVELOPMENT)
"Subscribed to" "Subscribed to"
} }
prefs.edit().putBoolean("subscribedToFBDebugChannel", !prefs.getBoolean("subscribedToFBDebugChannel", false)).apply() prefs.edit().putBoolean("subscribedToFBDebugChannel", !prefs.getBoolean("subscribedToFBDebugChannel", false)).apply()
@@ -475,10 +466,10 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
} }
this == "_ioschannel" -> { this == "_ioschannel" -> {
val subbed = if (prefs.getBoolean("subscribedToiOSChannel", false)) { val subbed = if (prefs.getBoolean("subscribedToiOSChannel", false)) {
unsubscribeFromTopic(Topic.IOS) unsubscribeFromTopic(SubstUtil.FB_TOPIC_IOS)
"Unsubscribed from" "Unsubscribed from"
} else { } else {
subscribeToTopic(Topic.IOS) subscribeToTopic(SubstUtil.FB_TOPIC_IOS)
"Subscribed to" "Subscribed to"
} }
prefs.edit().putBoolean("subscribedToiOSChannel", !prefs.getBoolean("subscribedToiOSChannel", false)).apply() prefs.edit().putBoolean("subscribedToiOSChannel", !prefs.getBoolean("subscribedToiOSChannel", false)).apply()
@@ -570,26 +561,6 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
return colours return colours
} }
private fun getRingtones(): ArrayList<Ringtone> {
lateinit var ringtoneCursor: Cursor
val ringtoneManager = RingtoneManager(activity).apply {
setType(RingtoneManager.TYPE_NOTIFICATION)
ringtoneCursor = cursor
}
val alarms = ArrayList<Ringtone>()
while (!ringtoneCursor.isAfterLast && ringtoneCursor.moveToNext()) {
val position = ringtoneCursor.position
alarms.add(
Ringtone(
ringtoneManager.getRingtone(position).getTitle(mContext),
ringtoneManager.getRingtoneUri(position).toString()
)
)
}
return alarms
}
// Functions for handling other things // Functions for handling other things
private fun setRingtoneText() { private fun setRingtoneText() {
binding.textCustomiseRingtoneDesc.text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { binding.textCustomiseRingtoneDesc.text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -620,16 +591,20 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
"\n\nSubscribed to dev channel: ${prefs.getBoolean("subscribedToFBDebugChannel", false)}" "\n\nSubscribed to dev channel: ${prefs.getBoolean("subscribedToFBDebugChannel", false)}"
} }
private fun subscribeToTopic(topic: Topic) = FirebaseMessaging.getInstance().subscribeToTopic(topic.tag) private fun subscribeToTopic(topic: String) = FirebaseMessaging.getInstance().subscribeToTopic(topic)
private fun unsubscribeFromTopic(topic: Topic) = FirebaseMessaging.getInstance().unsubscribeFromTopic(topic.tag) private fun unsubscribeFromTopic(topic: String) = FirebaseMessaging.getInstance().unsubscribeFromTopic(topic)
// Below this point follow string literals that I didn't bother putting in /res/values/strings // Below this point follow string literals that I didn't bother putting in /res/values/strings
private val licences = "Libraries:" + // TODO check if this is displayed properly
"\n • jsoup HTML parser © 2009-2018 Jonathan Hedley, licensed under the open source MIT Licence" + private val licences = """Libraries:
"\n\nFont:" + • jsoup HTML parser © 2009-2018 Jonathan Hedley, licensed under the open source MIT Licence
"\n • Manrope © 2018-2019 Michael Sharanda, licensed under the SIL Open Font Licence 1.1" + Font:
"\n\nIcons:" + • Manrope © 2018-2019 Michael Sharanda, licensed under the SIL Open Font Licence 1.1
"\n • bqlqn\n • fjstudio\n • Freepik\n • Smashicons" + Icons:
"\n • © 2013-2019 Freepik Company S.L., licensed under Creative Commons BY 3.0" • bqlqn
• fjstudio
• Freepik
• Smashicons
• © 2013-2019 Freepik Company S.L., licensed under Creative Commons BY 3.0""".trimIndent()
} }
@@ -1,7 +1,8 @@
package com.denizd.substitutionplan.services package com.denizd.substitutionplan.services
import android.annotation.SuppressLint import android.annotation.SuppressLint
import com.denizd.substitutionplan.data.DataFetcher import com.denizd.substitutionplan.data.Caller
import com.denizd.substitutionplan.database.SubstRepository
import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage import com.google.firebase.messaging.RemoteMessage
@@ -13,13 +14,6 @@ import com.google.firebase.messaging.RemoteMessage
internal class FBNotificationService : FirebaseMessagingService() { internal class FBNotificationService : FirebaseMessagingService() {
override fun onMessageReceived(p0: RemoteMessage) { override fun onMessageReceived(p0: RemoteMessage) {
DataFetcher( SubstRepository(application).fetchDataOnline(Caller.JOBSERVICE)
isPlan = true,
isMenu = true,
isJobService = true,
context = applicationContext,
application = application,
parentView = null
).execute()
} }
} }
@@ -3,7 +3,7 @@ package com.denizd.substitutionplan.services
import android.app.job.JobParameters import android.app.job.JobParameters
import android.app.job.JobService import android.app.job.JobService
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.denizd.substitutionplan.data.Topic import com.denizd.substitutionplan.data.SubstUtil
import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseApp
import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessaging
@@ -35,24 +35,24 @@ internal class FBPingService : JobService() {
if (!prefs.getBoolean("firstTime", true)) { if (!prefs.getBoolean("firstTime", true)) {
FirebaseMessaging.getInstance().apply { FirebaseMessaging.getInstance().apply {
if (prefs.getBoolean("notif", true)) { if (prefs.getBoolean("notif", true)) {
subscribeToTopic(Topic.ANDROID.tag) subscribeToTopic(SubstUtil.FB_TOPIC_ANDROID)
} else { } else {
unsubscribeFromTopic(Topic.ANDROID.tag) unsubscribeFromTopic(SubstUtil.FB_TOPIC_ANDROID)
} }
if (prefs.getBoolean("subscribedToFBDebugChannel", false)) { if (prefs.getBoolean("subscribedToFBDebugChannel", false)) {
subscribeToTopic(Topic.DEVELOPMENT.tag) subscribeToTopic(SubstUtil.FB_TOPIC_DEVELOPMENT)
} else { } else {
unsubscribeFromTopic(Topic.DEVELOPMENT.tag) unsubscribeFromTopic(SubstUtil.FB_TOPIC_DEVELOPMENT)
} }
if (prefs.getBoolean("subscribedToiOSChannel", false)) { if (prefs.getBoolean("subscribedToiOSChannel", false)) {
subscribeToTopic(Topic.IOS.tag) subscribeToTopic(SubstUtil.FB_TOPIC_IOS)
} else { } else {
unsubscribeFromTopic(Topic.IOS.tag) unsubscribeFromTopic(SubstUtil.FB_TOPIC_IOS)
} }
subscribeToTopic(Topic.BROADCAST.tag) subscribeToTopic(SubstUtil.FB_TOPIC_BROADCAST)
prefs.edit().putInt("pingFB", prefs.getInt("pingFB", 0) + 1).apply() prefs.edit().putInt("pingFB", prefs.getInt("pingFB", 0) + 1).apply()
} }
@@ -0,0 +1,31 @@
package com.denizd.substitutionplan.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import com.denizd.substitutionplan.data.Caller
import com.denizd.substitutionplan.database.SubstRepository
import com.denizd.substitutionplan.models.Food
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
internal class FoodViewModel(application: Application) : AndroidViewModel(application) {
private val repo = SubstRepository(application)
val allFoodItems: LiveData<List<Food>>?
val emptyFoodMenu = repo.emptyFoodMenu
val shouldAutoRefresh = repo.shouldAutoRefresh
init {
allFoodItems = repo.allFoodItems
}
fun refresh(updateUi: (result: String, error: Boolean) -> Unit) {
GlobalScope.launch {
val task = GlobalScope.async { repo.fetchDataOnline(Caller.FOODMENU) }
val result = task.await()
updateUi(result.first, result.second)
}
}
}
@@ -0,0 +1,25 @@
package com.denizd.substitutionplan.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import com.denizd.substitutionplan.database.SettingsRepository
import com.denizd.substitutionplan.models.Ringtone
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
internal class SettingsViewModel(application: Application) : AndroidViewModel(application) {
private val repo = SettingsRepository(application)
lateinit var ringtones: List<Ringtone>
val ringtonesInitialised = ::ringtones.isInitialized
fun getRingtones(updateUi: () -> Unit) {
GlobalScope.launch {
val task = GlobalScope.async { repo.ringtones }
ringtones = task.await()
updateUi()
}
}
}
@@ -0,0 +1,47 @@
package com.denizd.substitutionplan.viewmodels
import android.app.Application
import android.content.res.Configuration
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import com.denizd.substitutionplan.data.Caller
import com.denizd.substitutionplan.database.SubstRepository
import com.denizd.substitutionplan.models.Substitution
import kotlinx.coroutines.*
internal class SubstViewModel(application: Application) : AndroidViewModel(application) {
private val repo = SubstRepository(application)
val allSubstitutionsSorted: LiveData<List<Substitution>>?
val allSubstitutionsOriginal: LiveData<List<Substitution>>?
val substitutionPlanColours = repo.substitutionPlanColours
val shouldAutoRefresh = repo.shouldAutoRefresh
val shouldUseAppSorting = repo.shouldUseAppSorting
val emptyGeneralPlan = repo.emptyGeneralPlan
val emptyPersonalSubstitution = repo.emptyPersonalSubstitution
init {
allSubstitutionsSorted = repo.allSubstitutionsSorted
allSubstitutionsOriginal = repo.allSubstitutionsOriginal
}
fun getNonNullString(key: String): String {
return repo.externalNonNullString(key)
}
fun checkIfSubstitutionPersonal(substitution: Substitution, classPreference: String, coursePreference: String, includesPsa: Boolean): Boolean {
return repo.checkIfSubstitutionPersonal(substitution, classPreference, coursePreference, includesPsa)
}
fun getGridColumnCount(config: Configuration): Int {
return repo.getGridColumnCount(config)
}
fun refresh(updateUi: (result: String, error: Boolean) -> Unit) {
GlobalScope.launch {
val task = GlobalScope.async { repo.fetchDataOnline(Caller.SUBSTITUTION) }
val result = task.await()
updateUi(result.first, result.second)
}
}
}
+3 -1
View File
@@ -2,7 +2,7 @@
<resources> <resources>
<string name="app_name">AvH-Plan</string> <string name="app_name">AvH-Plan</string>
<string name="filters">Filter</string> <string name="filters">Filter</string>
<string name="no_internet_connection">Keine Internetverbindung</string> <string name="an_error_occurred">Ein Fehler ist aufgetreten</string>
<string name="general">Allgemein</string> <string name="general">Allgemein</string>
<string name="grade">Klasse</string> <string name="grade">Klasse</string>
<string name="courses">Kurse</string> <string name="courses">Kurse</string>
@@ -192,4 +192,6 @@
<item>Weiß</item> <item>Weiß</item>
<item>Schwarz</item> <item>Schwarz</item>
</string-array> </string-array>
<string name="plan_updated">Der Plan wurde aktualisiert</string>
<string name="food_menu_updated">Der Speiseplan wurde aktualisiert</string>
</resources> </resources>
+3 -1
View File
@@ -3,7 +3,7 @@
<string name="app_name">AvH Plan</string> <string name="app_name">AvH Plan</string>
<string name="substitution_plan">Substitution Plan</string> <string name="substitution_plan">Substitution Plan</string>
<string name="filters">Filters</string> <string name="filters">Filters</string>
<string name="no_internet_connection">No internet connection</string> <string name="an_error_occurred">An error has occurred</string>
<string name="general">General</string> <string name="general">General</string>
<string name="grade">Grade</string> <string name="grade">Grade</string>
<string name="courses">Courses</string> <string name="courses">Courses</string>
@@ -205,4 +205,6 @@
<item>White</item> <item>White</item>
<item>Black</item> <item>Black</item>
</string-array> </string-array>
<string name="plan_updated">The plan has been updated</string>
<string name="food_menu_updated">The food menu has been updated</string>
</resources> </resources>