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
@@ -2,7 +2,6 @@ package com.denizd.substitutionplan.adapters
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.text.SpannableString
import android.text.style.StrikethroughSpan
@@ -26,9 +25,9 @@ import java.util.*
* PlanFragment.kt and its subclasses
*
* @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
@@ -114,11 +113,7 @@ internal class SubstitutionAdapter(private var substitutions: List<Substitution>
View.GONE
} else {
val colourString = SubstUtil.getColourString(holder.course.text.toString())
val colourPrefsInt = if (colourString.isNotEmpty()) {
prefs.getString("card$colourString", "") ?: ""
} else {
""
}
val colourPrefsInt = colours[colourString] ?: ""
colour = SubstUtil.getColourForString(colourPrefsInt, holder.context)
cardBackgroundColour = if (colour != 0) {
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"
fun getEmptyGeneralSubstitution(context: Context) = arrayOf(
Substitution("", "", "", context.getString(R.string.plan_empty), "", "", "", "", 0, 0, 0)
).toList()
fun getEmptyPersonalSubstitution(context: Context) =
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()
// Constants for Firebase Cloud Messaging Topics
const val FB_TOPIC_ANDROID = "substitutions-android"
const val FB_TOPIC_BROADCAST = "substitutions-broadcast"
const val FB_TOPIC_IOS = "substitutions-ios"
const val FB_TOPIC_DEVELOPMENT = "substitutions-debug"
/**
* This function returns an array that contains all data relevant to tinting individual entries
@@ -248,39 +243,6 @@ internal object SubstUtil {
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
* 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? {
if (instance == null) {
fun getSubstInstance(context: Context): SubstDatabase? {
if (substInstance == null) {
synchronized (SubstDatabase::class) {
instance = Room.databaseBuilder(context.applicationContext,
substInstance = Room.databaseBuilder(context.applicationContext,
SubstDatabase::class.java,
"subst_database")
.addMigrations(addTeacherColumn, addTypeColumn, addDatePriorityColumn, addWebsitePriorityColumn)
@@ -50,7 +50,7 @@ internal abstract class SubstDatabase : RoomDatabase() {
.build()
}
}
return instance
return substInstance
}
private var foodInstance: SubstDatabase? = null
@@ -1,30 +1,379 @@
package com.denizd.substitutionplan.database
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.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 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 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 {
val database =
SubstDatabase.getInstance(application)
substDao = database?.substDao()
allSubstitutionsSorted = substDao?.allSubstitutionsSorted
allSubstitutionsOriginal = substDao?.allSubstitutionsOriginal
val substInstance = SubstDatabase.getSubstInstance(application)
val foodInstance = SubstDatabase.getFoodInstance(application)
substitutionDao = substInstance?.substDao()
foodMenuDao = foodInstance?.substDao()
allSubstitutionsSorted = substitutionDao?.allSubstitutionsSorted
allSubstitutionsOriginal = substitutionDao?.allSubstitutionsOriginal
allFoodItems = foodMenuDao?.allFoods
}
fun insert(substitution: Substitution) {
substDao?.insertSubst(substitution)
private fun insert(substitution: Substitution) {
substitutionDao?.insertSubst(substitution)
}
fun deleteAllSubst() {
substDao?.deleteAllSubst()
private fun insert(food: Food) {
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
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import androidx.preference.PreferenceManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.GridLayoutManager
import com.denizd.substitutionplan.R
import com.denizd.substitutionplan.adapters.FoodAdapter
import com.denizd.substitutionplan.data.SubstUtil
import com.denizd.substitutionplan.database.FoodViewModel
import com.denizd.substitutionplan.viewmodels.FoodViewModel
import com.denizd.substitutionplan.databinding.FoodLayoutBinding
import com.denizd.substitutionplan.models.Food
import com.google.android.material.snackbar.Snackbar
internal class FoodFragment : Fragment() {
private val mAdapter = FoodAdapter(ArrayList())
private lateinit var mContext: Context
private lateinit var prefs: SharedPreferences
private lateinit var foodViewModel: FoodViewModel
private lateinit var viewModel: FoodViewModel
private lateinit var binding: FoodLayoutBinding
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
prefs = PreferenceManager.getDefaultSharedPreferences(mContext)
}
private lateinit var snackBarContainer: CoordinatorLayout
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FoodLayoutBinding.inflate(inflater, container, false)
@@ -40,28 +33,41 @@ internal class FoodFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
snackBarContainer = view.rootView.findViewById(R.id.coordination) // TODO replace findViewById with ViewBinding
binding.recyclerView.apply {
hasFixedSize()
layoutManager = GridLayoutManager(mContext, 1)
layoutManager = GridLayoutManager(requireContext(), 1)
adapter = mAdapter
}
foodViewModel = ViewModelProviders.of(this).get(FoodViewModel::class.java)
foodViewModel.allFoods?.observe(this, Observer<List<Food>> { foodList ->
viewModel = ViewModelProviders.of(this).get(FoodViewModel::class.java)
viewModel.allFoodItems?.observe(this, Observer<List<Food>> { foodList ->
binding.recyclerView.scheduleLayoutAnimation()
mAdapter.setFood(if (foodList.isEmpty()) {
SubstUtil.getEmptyFoodMenu(mContext)
viewModel.emptyFoodMenu
} else {
foodList
})
})
if (prefs.getBoolean("autoRefresh", false)) {
foodViewModel.refresh(swipeRefreshLayout = binding.swipeRefreshLayout, rootView = view.rootView)
if (viewModel.shouldAutoRefresh) {
refreshAndDisplaySnackBar()
}
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.view.View
import androidx.lifecycle.Observer
import com.denizd.substitutionplan.data.SubstUtil
import com.denizd.substitutionplan.models.Substitution
internal class GeneralPlanFragment : PlanFragment() {
@@ -13,7 +12,7 @@ internal class GeneralPlanFragment : PlanFragment() {
substitutionPlan?.observe(this, Observer<List<Substitution>> { substitutions ->
mAdapter.setSubst(if (substitutions.isEmpty()) {
SubstUtil.getEmptyGeneralSubstitution(mContext)
viewModel.emptyGeneralPlan
} else {
substitutions
})
@@ -3,7 +3,6 @@ package com.denizd.substitutionplan.fragments
import android.os.Bundle
import android.view.View
import androidx.lifecycle.Observer
import com.denizd.substitutionplan.data.SubstUtil
import com.denizd.substitutionplan.models.Substitution
internal class PersonalPlanFragment : PlanFragment() {
@@ -11,30 +10,28 @@ internal class PersonalPlanFragment : PlanFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val coursePreference = prefs.getString("courses", "") ?: ""
val classPreference = prefs.getString("classes", "") ?: ""
val coursePreference = viewModel.getNonNullString("courses")
val classPreference = viewModel.getNonNullString("classes")
substitutionPlan?.observe(this, Observer<List<Substitution>> { substitutions ->
planCardList.clear()
isPersonalPlanEmpty = true
binding.recyclerView.visibility = View.VISIBLE
val list = ArrayList<Substitution>()
substitutions.filter { substItem ->
SubstUtil.checkPersonalSubstitutions(
viewModel.checkIfSubstitutionPersonal(
substItem,
coursePreference,
classPreference,
coursePreference,
true
)
}.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) {
planCardList.add(SubstUtil.getEmptyPersonalSubstitution(mContext))
list.add(viewModel.emptyPersonalSubstitution)
}
binding.recyclerView.scheduleLayoutAnimation()
mAdapter.setSubst(planCardList)
mAdapter.setSubst(list)
})
}
}
@@ -1,43 +1,33 @@
package com.denizd.substitutionplan.fragments
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.Bundle
import android.os.Handler
import androidx.preference.PreferenceManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.GridLayoutManager
import com.denizd.substitutionplan.R
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.models.Substitution
import com.google.android.material.snackbar.Snackbar
import kotlin.collections.ArrayList
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 handler = Handler()
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)
internal val mAdapter: SubstitutionAdapter by lazy {
SubstitutionAdapter(ArrayList(), viewModel.substitutionPlanColours)
}
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? {
binding = PlanBinding.inflate(inflater, container, false)
@@ -46,51 +36,50 @@ internal open class PlanFragment : Fragment() {
override fun onConfigurationChanged(newConfig: Configuration) {
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?) {
super.onViewCreated(view, savedInstanceState)
substViewModel = ViewModelProviders.of(this).get(SubstViewModel::class.java)
substitutionPlan = if (prefs.getBoolean("app_specific_sorting", true)) {
substViewModel.allSubstitutionsSorted
snackBarContainer = view.rootView.findViewById(R.id.coordination) // TODO replace findViewById with ViewBinding
viewModel = ViewModelProviders.of(this).get(SubstViewModel::class.java)
substitutionPlan = if (viewModel.shouldUseAppSorting) {
viewModel.allSubstitutionsSorted
} else {
substViewModel.allSubstitutionsOriginal
viewModel.allSubstitutionsOriginal
}
mAdapter = SubstitutionAdapter(planCardList, prefs)
binding.recyclerView.apply {
hasFixedSize()
layoutManager = GridLayoutManager(mContext, getGridColumnCount(resources.configuration))
layoutManager = GridLayoutManager(requireContext(), viewModel.getGridColumnCount(resources.configuration))
adapter = mAdapter
}
if (prefs.getInt("firstTimeOpening", 0) == 1) {
substViewModel.refresh(swipeRefreshLayout = binding.swipeRefreshLayout, rootView = view.rootView, refreshMenu = true)
prefs.edit().putInt("firstTimeOpening", 2).apply()
}
// TODO figure something out
// if (prefs.getInt("firstTimeOpening", 0) == 1) {
// refreshAndDisplaySnackBar()
// prefs.edit().putInt("firstTimeOpening", 2).apply()
// }
if (prefs.getBoolean("autoRefresh", false)) {
substViewModel.refresh(swipeRefreshLayout = binding.swipeRefreshLayout, rootView = view.rootView, refreshMenu = false)
if (viewModel.shouldAutoRefresh) {
refreshAndDisplaySnackBar()
}
binding.swipeRefreshLayout.setOnRefreshListener {
substViewModel.refresh(swipeRefreshLayout = binding.swipeRefreshLayout, rootView = view.rootView, refreshMenu = false)
refreshAndDisplaySnackBar()
}
}
private fun getGridColumnCount(config: Configuration): Int {
val tabletSize = resources.getBoolean(R.bool.isTablet)
return if (tabletSize) {
when (config.orientation) {
Configuration.ORIENTATION_PORTRAIT -> 2
else -> 3
}
} else {
when (config.orientation) {
Configuration.ORIENTATION_PORTRAIT -> 1
else -> 2
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)) // TODO check if requireContext() is dangerous ;(
}
snackBar.show()
binding.swipeRefreshLayout.isRefreshing = false
}
}
}
@@ -5,8 +5,6 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.database.Cursor
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
@@ -17,25 +15,23 @@ import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.denizd.substitutionplan.*
import com.denizd.substitutionplan.adapters.ColourPickerAdapter
import com.denizd.substitutionplan.adapters.CourseColourAdapter
import com.denizd.substitutionplan.adapters.RingtoneAdapter
import com.denizd.substitutionplan.data.DataFetcher
import com.denizd.substitutionplan.data.SubstUtil
import com.denizd.substitutionplan.data.Topic
import com.denizd.substitutionplan.databinding.ContentSettingsBinding
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.button.MaterialButton
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 prefs: SharedPreferences
private val customTabsIntent = CustomTabsIntent.Builder().build() as CustomTabsIntent
private var window: Window? = null
private var longPressed = false
private var customiseColoursButtonLongPressed = false
private var versionCount = 0
private var currentCourseSelectedInColourPicker = ""
private val ringtones: List<Ringtone> by lazy {
getRingtones()
}
private lateinit var colourRecycler: RecyclerView
private lateinit var ringtoneDialog: AlertDialog
private lateinit var colourPickerDialog: AlertDialog
private lateinit var binding: ContentSettingsBinding
private lateinit var textCustomiseColoursTitle: TextView
private lateinit var textCustomiseColoursDesc: TextView
private lateinit var viewModel: SettingsViewModel
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
prefs = PreferenceManager.getDefaultSharedPreferences(mContext)
window = activity?.window
}
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?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this)[SettingsViewModel::class.java]
binding.apply {
val thisFragment = this@SettingsFragment
thisFragment.textCustomiseColoursTitle = textCustomiseColoursTitle
thisFragment.textCustomiseColoursDesc = textCustomiseColoursDesc
// Set text
setRingtoneText()
@@ -174,15 +162,15 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
if (Build.VERSION.SDK_INT in 23..28) {
@SuppressLint("InlinedApi")
window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
window?.navigationBarColor = ContextCompat.getColor(mContext, R.color.colorBackground)
activity?.window?.decorView?.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
activity?.window?.navigationBarColor = ContextCompat.getColor(mContext, R.color.colorBackground)
}
}
2 -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) // only accessible on API 28+
else -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
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_order_help -> createDialog(getString(R.string.ordering_systems_dialog_title), getString(R.string.ordering_systems_dialog_text))
R.id.button_forced_refresh -> {
DataFetcher(
isPlan = true,
isMenu = true,
forced = true,
context = mContext,
application = activity!!.application,
parentView = view!!.rootView
).execute()
// val repo = SubstRepository(activity?.application!!)
// repo.putAndApplyString("timeNew", "")
// repo.putAndApplyString("newFoodTime", "")
// repo.fetchDataOnline(Caller.SETTINGS)
// TODO needs async
// TODO put in viewModel
// TODO create viewModel
// TODO remove TODOs
}
R.id.button_visit_website -> {
try {
@@ -250,14 +238,14 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
override fun onLongClick(v: View?): Boolean {
return when (v?.id) {
R.id.button_customise_colours -> {
if (!longPressed) {
textCustomiseColoursTitle.text = getString(R.string.made_by_deniz)
textCustomiseColoursDesc.text = getString(R.string.thanks_for_using)
if (!customiseColoursButtonLongPressed) {
binding.textCustomiseColoursTitle.text = getString(R.string.made_by_deniz)
binding.textCustomiseColoursDesc.text = getString(R.string.thanks_for_using)
} else {
textCustomiseColoursTitle.text = getString(R.string.customise_colours_title)
textCustomiseColoursDesc.text = getString(R.string.customise_colours_desc)
binding.textCustomiseColoursTitle.text = getString(R.string.customise_colours_title)
binding.textCustomiseColoursDesc.text = getString(R.string.customise_colours_desc)
}
longPressed = !longPressed
customiseColoursButtonLongPressed = !customiseColoursButtonLongPressed
true
}
R.id.button_courses_help -> {
@@ -298,9 +286,9 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
R.id.switch_notifications -> {
prefs.edit().putBoolean("notif", isChecked).apply()
if (isChecked) {
subscribeToTopic(Topic.ANDROID)
subscribeToTopic(SubstUtil.FB_TOPIC_ANDROID)
} else {
unsubscribeFromTopic(Topic.ANDROID)
unsubscribeFromTopic(SubstUtil.FB_TOPIC_ANDROID)
}
}
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)
titleText.text = getString(R.string.pick_ringtone_dialog_title)
val recycler = dialogView.findViewById<RecyclerView>(R.id.recyclerView)
recycler.apply {
dialogView.findViewById<RecyclerView>(R.id.recyclerView).apply {
hasFixedSize()
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()
@@ -464,10 +455,10 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
}
this == "_devchannel" -> {
val subbed = if (prefs.getBoolean("subscribedToFBDebugChannel", false)) {
unsubscribeFromTopic(Topic.DEVELOPMENT)
unsubscribeFromTopic(SubstUtil.FB_TOPIC_DEVELOPMENT)
"Unsubscribed from"
} else {
subscribeToTopic(Topic.DEVELOPMENT)
subscribeToTopic(SubstUtil.FB_TOPIC_DEVELOPMENT)
"Subscribed to"
}
prefs.edit().putBoolean("subscribedToFBDebugChannel", !prefs.getBoolean("subscribedToFBDebugChannel", false)).apply()
@@ -475,10 +466,10 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
}
this == "_ioschannel" -> {
val subbed = if (prefs.getBoolean("subscribedToiOSChannel", false)) {
unsubscribeFromTopic(Topic.IOS)
unsubscribeFromTopic(SubstUtil.FB_TOPIC_IOS)
"Unsubscribed from"
} else {
subscribeToTopic(Topic.IOS)
subscribeToTopic(SubstUtil.FB_TOPIC_IOS)
"Subscribed to"
}
prefs.edit().putBoolean("subscribedToiOSChannel", !prefs.getBoolean("subscribedToiOSChannel", false)).apply()
@@ -570,26 +561,6 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
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
private fun setRingtoneText() {
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)}"
}
private fun subscribeToTopic(topic: Topic) = FirebaseMessaging.getInstance().subscribeToTopic(topic.tag)
private fun unsubscribeFromTopic(topic: Topic) = FirebaseMessaging.getInstance().unsubscribeFromTopic(topic.tag)
private fun subscribeToTopic(topic: String) = FirebaseMessaging.getInstance().subscribeToTopic(topic)
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
private val licences = "Libraries:" +
"\n • jsoup HTML parser © 2009-2018 Jonathan Hedley, licensed under the open source MIT Licence" +
"\n\nFont:" +
"\n • Manrope © 2018-2019 Michael Sharanda, licensed under the SIL Open Font Licence 1.1" +
"\n\nIcons:" +
"\n • bqlqn\n • fjstudio\n • Freepik\n • Smashicons" +
"\n • © 2013-2019 Freepik Company S.L., licensed under Creative Commons BY 3.0"
// TODO check if this is displayed properly
private val licences = """Libraries:
• jsoup HTML parser © 2009-2018 Jonathan Hedley, licensed under the open source MIT Licence
Font:
• Manrope © 2018-2019 Michael Sharanda, licensed under the SIL Open Font Licence 1.1
Icons:
• 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
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.RemoteMessage
@@ -13,13 +14,6 @@ import com.google.firebase.messaging.RemoteMessage
internal class FBNotificationService : FirebaseMessagingService() {
override fun onMessageReceived(p0: RemoteMessage) {
DataFetcher(
isPlan = true,
isMenu = true,
isJobService = true,
context = applicationContext,
application = application,
parentView = null
).execute()
SubstRepository(application).fetchDataOnline(Caller.JOBSERVICE)
}
}
@@ -3,7 +3,7 @@ package com.denizd.substitutionplan.services
import android.app.job.JobParameters
import android.app.job.JobService
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.messaging.FirebaseMessaging
@@ -35,24 +35,24 @@ internal class FBPingService : JobService() {
if (!prefs.getBoolean("firstTime", true)) {
FirebaseMessaging.getInstance().apply {
if (prefs.getBoolean("notif", true)) {
subscribeToTopic(Topic.ANDROID.tag)
subscribeToTopic(SubstUtil.FB_TOPIC_ANDROID)
} else {
unsubscribeFromTopic(Topic.ANDROID.tag)
unsubscribeFromTopic(SubstUtil.FB_TOPIC_ANDROID)
}
if (prefs.getBoolean("subscribedToFBDebugChannel", false)) {
subscribeToTopic(Topic.DEVELOPMENT.tag)
subscribeToTopic(SubstUtil.FB_TOPIC_DEVELOPMENT)
} else {
unsubscribeFromTopic(Topic.DEVELOPMENT.tag)
unsubscribeFromTopic(SubstUtil.FB_TOPIC_DEVELOPMENT)
}
if (prefs.getBoolean("subscribedToiOSChannel", false)) {
subscribeToTopic(Topic.IOS.tag)
subscribeToTopic(SubstUtil.FB_TOPIC_IOS)
} 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()
}
@@ -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)
}
}
}