diff --git a/app/src/main/java/com/denizd/substitutionplan/activities/FirstTime.kt b/app/src/main/java/com/denizd/substitutionplan/activities/FirstTime.kt index 3450057..7325bdd 100644 --- a/app/src/main/java/com/denizd/substitutionplan/activities/FirstTime.kt +++ b/app/src/main/java/com/denizd/substitutionplan/activities/FirstTime.kt @@ -1,7 +1,6 @@ package com.denizd.substitutionplan.activities import android.animation.LayoutTransition -import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.content.SharedPreferences @@ -18,16 +17,19 @@ import android.view.animation.AnimationUtils import android.widget.* import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.app.AppCompatDelegate import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.ContextCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import com.denizd.substitutionplan.R +import com.denizd.substitutionplan.data.HelperFunctions import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.textfield.TextInputEditText import java.util.* import kotlin.math.hypot +/** + * This class is launched after a successful login and enables the user to set any preferences + * without scouring through the app's settings first + */ internal class FirstTime : AppCompatActivity(R.layout.activity_first_time) { private lateinit var context: Context @@ -42,21 +44,7 @@ internal class FirstTime : AppCompatActivity(R.layout.activity_first_time) { val parentLayout = findViewById(R.id.coordinatorLayout) parentLayout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) - val barColour = when { - Build.VERSION.SDK_INT < Build.VERSION_CODES.M -> ContextCompat.getColor(context, R.color.legacyBlack) - Build.VERSION.SDK_INT <= Build.VERSION_CODES.P -> ContextCompat.getColor(context, R.color.colorBackground) - else -> 0 - } - if (barColour != 0) { - window.navigationBarColor = barColour - window.statusBarColor = barColour - } - if (AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_NO) { - if (Build.VERSION.SDK_INT in 23..28) { - @SuppressLint("InlinedApi") - window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - } - } + HelperFunctions.setTheme(window = window, context = context) prefs = PreferenceManager.getDefaultSharedPreferences(context) as SharedPreferences val edit = prefs.edit() @@ -89,6 +77,9 @@ internal class FirstTime : AppCompatActivity(R.layout.activity_first_time) { val inflater = this.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + /** + * Setup is finished; trigger an animation and save the user's settings in Shared Preferences + */ fab.setOnClickListener { fab.isClickable = false diff --git a/app/src/main/java/com/denizd/substitutionplan/activities/Login.kt b/app/src/main/java/com/denizd/substitutionplan/activities/Login.kt index b57ff8e..88444e6 100644 --- a/app/src/main/java/com/denizd/substitutionplan/activities/Login.kt +++ b/app/src/main/java/com/denizd/substitutionplan/activities/Login.kt @@ -1,7 +1,6 @@ package com.denizd.substitutionplan.activities import android.animation.LayoutTransition -import android.annotation.SuppressLint import android.content.Intent import android.content.SharedPreferences import android.graphics.drawable.AnimatedVectorDrawable @@ -18,15 +17,18 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.app.AppCompatDelegate import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.ContextCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import com.denizd.substitutionplan.R +import com.denizd.substitutionplan.data.HelperFunctions import com.denizd.substitutionplan.data.LoginWebViewClient import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import kotlin.math.hypot +/** + * This class will be instantiated upon first launch of the app to present the user with a WebView + * allowing them to log in to the school website to verify them as being occupied at the gymnasium. + */ internal class Login : AppCompatActivity(R.layout.activity_login_webview), LoginWebViewClient.OnLoginSuccessListener { private lateinit var prefs: SharedPreferences @@ -41,21 +43,7 @@ internal class Login : AppCompatActivity(R.layout.activity_login_webview), Login val title = findViewById(R.id.txtWelcome) prefs = PreferenceManager.getDefaultSharedPreferences(this) - val barColour = when { - Build.VERSION.SDK_INT < Build.VERSION_CODES.M -> ContextCompat.getColor(this, R.color.legacyBlack) - Build.VERSION.SDK_INT <= Build.VERSION_CODES.P -> ContextCompat.getColor(this, R.color.colorBackground) - else -> 0 - } - if (barColour != 0) { - window.navigationBarColor = barColour - window.statusBarColor = barColour - } - if (AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_NO) { - if (Build.VERSION.SDK_INT in 23..28) { - @SuppressLint("InlinedApi") - window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - } - } + HelperFunctions.setTheme(window = window, context = this) logInButton = findViewById(R.id.loginFab) logInButton.setOnClickListener { diff --git a/app/src/main/java/com/denizd/substitutionplan/activities/Main.kt b/app/src/main/java/com/denizd/substitutionplan/activities/Main.kt index f905208..6676879 100644 --- a/app/src/main/java/com/denizd/substitutionplan/activities/Main.kt +++ b/app/src/main/java/com/denizd/substitutionplan/activities/Main.kt @@ -35,6 +35,12 @@ import com.google.firebase.FirebaseApp import java.lang.IllegalArgumentException import java.util.* +/** + * The main class of the app. This controls navigation and enables the user to navigate through the + * app's fragments and view the information provided on the substitution table. Theming as well as + * setting up Firebase Cloud Messaging and Crashlytics are handled here. Also checks whether + * the user has completed the setup (logging in, setting preferences) + */ internal class Main : AppCompatActivity(R.layout.app_bar_main) { private lateinit var context: Context @@ -49,11 +55,16 @@ internal class Main : AppCompatActivity(R.layout.app_bar_main) { val login = Intent(context, Login::class.java) Crashlytics.setUserIdentifier(prefs.getString("username", "") ?: "") + /** + * This cascade of if-else's allows the user to return to any point of the setup if they + * decide to postpone it, e.g. logging in but not setting up their preferences. Furthermore, + * it prevents existing users from upgrading from a previous version of the app and skip the + * mandatory login + */ if (!prefs.getBoolean("successful_login", false)) { startActivity(login) finish() } else if (prefs.getBoolean("firstTime", true)) { -// } else if (true) { startActivity(firstTime) finish() } else { @@ -199,6 +210,12 @@ internal class Main : AppCompatActivity(R.layout.app_bar_main) { } } + /** + * Picks a greeting according to the time of day to present to the user if they choose to + * enable greetings + * + * @return the greeting + */ private fun getGreetingString(): String { val gen = Random() return String.format(when (Calendar.getInstance().get(Calendar.HOUR_OF_DAY)) { @@ -230,6 +247,9 @@ internal class Main : AppCompatActivity(R.layout.app_bar_main) { return true } + /** + * Triggers FBPingService.kt to re-subscribe to the selected Firebase topics every 15 minutes + */ private fun pingFirebaseTopics() { val componentName = ComponentName(this, FBPingService::class.java) val info = JobInfo.Builder(42, componentName) @@ -241,6 +261,12 @@ internal class Main : AppCompatActivity(R.layout.app_bar_main) { scheduler.schedule(info) } + /** + * Decides on the title for PersonalPlanFragment.kt. Picks a generic "Your plan" if greetings + * are enabled or "$user's Plan" if they are disabled + * + * @return the title + */ private fun personalPlanTitle(): String { val user = prefs.getString("username", "") ?: "" return if (prefs.getBoolean("greeting", true) || user.isEmpty()) { diff --git a/app/src/main/java/com/denizd/substitutionplan/adapters/CardAdapter.kt b/app/src/main/java/com/denizd/substitutionplan/adapters/CardAdapter.kt index 5835f7b..3312d04 100644 --- a/app/src/main/java/com/denizd/substitutionplan/adapters/CardAdapter.kt +++ b/app/src/main/java/com/denizd/substitutionplan/adapters/CardAdapter.kt @@ -130,6 +130,13 @@ internal class CardAdapter(private var mSubst: List, private val prefs: S else -> R.color.colorText } }) + + holder.iconView.visibility = if (icon == R.drawable.ic_empty) { + View.GONE + } else { + View.VISIBLE + } + holder.card.setCardBackgroundColor(ContextCompat.getColor(holder.iconView.context, cardBackgroundColour)) holder.iconView.setImageResource(icon) holder.iconView.setColorFilter(textColor) diff --git a/app/src/main/java/com/denizd/substitutionplan/data/DataFetcher.kt b/app/src/main/java/com/denizd/substitutionplan/data/DataFetcher.kt index 26c1534..5138295 100644 --- a/app/src/main/java/com/denizd/substitutionplan/data/DataFetcher.kt +++ b/app/src/main/java/com/denizd/substitutionplan/data/DataFetcher.kt @@ -32,7 +32,13 @@ import kotlin.collections.ArrayList * * @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 a notification will be prepared and sent if this and isPlan is true + * @param isJobService enables sending of a notification if true + * @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 + * @param forced if true, times for food menu and substitution table will be overwritten + * with empty values to enable a forced refresh */ internal class DataFetcher(isPlan: Boolean, isMenu: Boolean, isJobService: Boolean, context: Context, application: Application, parentView: View?, forced: Boolean) : AsyncTask() { diff --git a/app/src/main/java/com/denizd/substitutionplan/data/HelperFunctions.kt b/app/src/main/java/com/denizd/substitutionplan/data/HelperFunctions.kt index 89bac48..731d292 100644 --- a/app/src/main/java/com/denizd/substitutionplan/data/HelperFunctions.kt +++ b/app/src/main/java/com/denizd/substitutionplan/data/HelperFunctions.kt @@ -1,6 +1,7 @@ package com.denizd.substitutionplan.data import android.Manifest +import android.annotation.SuppressLint import android.annotation.TargetApi import android.app.NotificationChannel import android.app.NotificationManager @@ -8,9 +9,14 @@ import android.content.Context import android.content.SharedPreferences import android.content.pm.PackageManager import android.graphics.Color +import android.os.Build import android.os.Environment +import android.view.View +import android.view.Window import android.widget.Toast +import androidx.appcompat.app.AppCompatDelegate import androidx.core.app.ActivityCompat.requestPermissions +import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat.checkSelfPermission import androidx.fragment.app.FragmentActivity import com.denizd.substitutionplan.R @@ -21,14 +27,24 @@ import java.io.FileOutputStream import java.io.OutputStreamWriter import javax.xml.parsers.DocumentBuilderFactory +/** + * An object class used for declaring functions and variables used by multiple classes throughout + * the project. As it is an object class, it can be accessed statically without initialisation + */ internal object HelperFunctions { + /** + * A language-independent list of all courses that is used to save the custom colours of any + * course in SharedPreferences without depending on the user's device language + */ val languageIndependentCourses = arrayOf("German", "English", "French", "Spanish", "Latin", "Turkish", "Chinese", "Arts", "Music", "Theatre", "Geography", "History", "Politics", "Philosophy", "Religion", "Maths", "Biology", "Chemistry", "Physics", "CompSci", "PhysEd", "GLL", "WAT", "Forder", "WP") + val colourNames = arrayOf("default", "red", "orange", "yellow", "green", "teal", "cyan", "blue", "purple", "pink", "brown", "grey", "pureWhite", "salmon", "tangerine", "banana", "flora", "spindrift", "sky", "orchid", "lavender", "carnation", "brown2", "pureBlack") + private val colourIntegers = intArrayOf( 0, R.color.bgRed, @@ -119,6 +135,14 @@ internal object HelperFunctions { } } + /** + * This function served as a way to transfer a deprecated method of storing user-defined + * course colours to a new method. It is not required otherwise, but still remains in + * the code, as users from older versions (2.1.3 and below) may wish to upgrade without + * losing their settings + * + * @param prefs a reference to the app's SharedPreferences + */ fun transferOldColourIntsToString(prefs: SharedPreferences) { var colour = "" val edit = prefs.edit() @@ -135,6 +159,17 @@ internal object HelperFunctions { 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 subst 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(subst: Subst, coursePreference: String, classPreference: String, psa: Boolean): Boolean { val group = subst.group val course = subst.course @@ -157,6 +192,38 @@ internal object HelperFunctions { 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 + * SettingsFragment.kt are only accessible after the inital setup and serve slightly + * different purposes, they use their own implementations + */ + fun setTheme(window: Window, context: Context) { + val barColour = when { + Build.VERSION.SDK_INT < Build.VERSION_CODES.M -> ContextCompat.getColor(context, R.color.legacyBlack) + Build.VERSION.SDK_INT <= Build.VERSION_CODES.P -> ContextCompat.getColor(context, R.color.colorBackground) + else -> 0 + } + if (barColour != 0) { + window.navigationBarColor = barColour + window.statusBarColor = barColour + } + if (AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_NO) { + if (Build.VERSION.SDK_INT in 23..28) { + @SuppressLint("InlinedApi") + window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } + } + } + + /** + * Returns the notification channel that is reponsible for handling substitution notifications + * on Android Oreo and above. If it does not exist, this function creates it + * + * @param context the context used for gathering the notification system service + * @param prefs a reference to the app's SharedPreferences + * @return the notification channel + */ @TargetApi(26) fun getNotificationChannel(context: Context, prefs: SharedPreferences): NotificationChannel { val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -175,6 +242,21 @@ internal object HelperFunctions { } } + fun checkStringForArray(s: String, checking: Array): Boolean { + checking.forEach { check -> + if (s.contains(check)) return true + } + return false + } + + /** + * The code below is used for saving and restoring user settings. However, it is currently + * inaccessible to the end user without entering a code in the hidden settings menu, + * as no interface has been created for it and it is not built around Android's Scoped Storage + * (which deprecates java.io.File and is likely to be required for file access on Android 11 + * and above). The functions can still be accessed by entering "_READ" and "_WRITE" + * respectively in the hidden settings menu + */ private val prefKeys = arrayOf("username", "classes", "courses", "greeting", "themeInt", "notif", "defaultPersonalised", "autoRefresh", "firstTimeDev", "launchDev", "pingFB", "subscribedToFBDebugChannel", "subscribedToiOSChannel") @@ -255,11 +337,4 @@ internal object HelperFunctions { requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 42) } } - - fun checkStringForArray(s: String, checking: Array): Boolean { - checking.forEach { check -> - if (s.contains(check)) return true - } - return false - } } diff --git a/app/src/main/java/com/denizd/substitutionplan/data/LoginWebViewClient.kt b/app/src/main/java/com/denizd/substitutionplan/data/LoginWebViewClient.kt index 442e501..0227618 100644 --- a/app/src/main/java/com/denizd/substitutionplan/data/LoginWebViewClient.kt +++ b/app/src/main/java/com/denizd/substitutionplan/data/LoginWebViewClient.kt @@ -5,7 +5,7 @@ import android.webkit.WebView import android.webkit.WebViewClient /** - * class that extends WebViewClient to provide a function that checks the webpage's cookies to + * Class that extends WebViewClient to provide a function that checks the webpage's cookies to * verify a login * * @param successListener a reference to the OnLoginSuccessListener implemented in an activity @@ -15,7 +15,7 @@ internal class LoginWebViewClient(successListener: OnLoginSuccessListener) : Web private val mSuccessListener = successListener /** - * onPageFinished has been overridden to check if the cookie "joomla_user_state=logged_in" + * OnPageFinished has been overridden to check if the cookie "joomla_user_state=logged_in" * exists, and returns true to the OnLoginSuccessListener * * @param view a reference to the WebView @@ -32,14 +32,14 @@ internal class LoginWebViewClient(successListener: OnLoginSuccessListener) : Web } /** - * the interface that provides onLoginSucceeded to the activity or fragment that implements it + * The interface that provides onLoginSucceeded to the activity or fragment that implements it */ internal interface OnLoginSuccessListener { /** - * this function can be overridden to execute code when a login has been successful + * This function can be overridden to execute code when a login has been successful * - * @param success boolean value that returns true if the cookie could be found, + * @param success boolean value that is set to true if the cookie could be found, * false otherwise */ fun onLoginSucceeded(success: Boolean) diff --git a/app/src/main/java/com/denizd/substitutionplan/data/Topic.kt b/app/src/main/java/com/denizd/substitutionplan/data/Topic.kt index bed3074..eea79ab 100644 --- a/app/src/main/java/com/denizd/substitutionplan/data/Topic.kt +++ b/app/src/main/java/com/denizd/substitutionplan/data/Topic.kt @@ -1,5 +1,9 @@ package com.denizd.substitutionplan.data +/** + * Enum class for declaring Firebase Cloud Messaging topics without using the raw strings + * throughout the project + */ internal enum class Topic(val tag: String) { ANDROID("substitutions-android"), BROADCAST("substitutions-broadcast"), diff --git a/app/src/main/java/com/denizd/substitutionplan/services/FBNotificationService.kt b/app/src/main/java/com/denizd/substitutionplan/services/FBNotificationService.kt index cd24c1d..0302104 100644 --- a/app/src/main/java/com/denizd/substitutionplan/services/FBNotificationService.kt +++ b/app/src/main/java/com/denizd/substitutionplan/services/FBNotificationService.kt @@ -4,6 +4,10 @@ import com.denizd.substitutionplan.data.DataFetcher import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage +/** + * This class handles Firebase data notifications and triggers a refresh of the substitution plan + * as well as the food menu upon arrival + */ internal class FBNotificationService : FirebaseMessagingService() { override fun onMessageReceived(p0: RemoteMessage) { diff --git a/app/src/main/java/com/denizd/substitutionplan/services/FBPingService.kt b/app/src/main/java/com/denizd/substitutionplan/services/FBPingService.kt index 6b79681..b63f870 100644 --- a/app/src/main/java/com/denizd/substitutionplan/services/FBPingService.kt +++ b/app/src/main/java/com/denizd/substitutionplan/services/FBPingService.kt @@ -7,6 +7,13 @@ import com.denizd.substitutionplan.data.Topic import com.google.firebase.FirebaseApp import com.google.firebase.messaging.FirebaseMessaging +/** + * This class regularly resubscribes to keep the app in the background. This is supposed to resolve + * issues regarding the app being force-closed in the background and not being able to receive + * notifications. + * + * I am unsure about its effectiveness, though notifications do seem to arrive + */ internal class FBPingService : JobService() { private var jobCancelled = false diff --git a/app/src/main/res/layout/app_bar_main.xml b/app/src/main/res/layout/app_bar_main.xml index 53bb2b7..eac22ab 100644 --- a/app/src/main/res/layout/app_bar_main.xml +++ b/app/src/main/res/layout/app_bar_main.xml @@ -52,6 +52,7 @@ android:layout_marginEnd="16dp" android:layout_marginTop="4dp" android:layout_marginBottom="4dp" + android:maxLines="1" app:layout_constraintTop_toBottomOf="@+id/text_greeting" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/course_card.xml b/app/src/main/res/layout/course_card.xml index 043c98a..0d59f04 100644 --- a/app/src/main/res/layout/course_card.xml +++ b/app/src/main/res/layout/course_card.xml @@ -46,6 +46,7 @@ @@ -53,7 +54,6 @@ android:id="@+id/course" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="4dp" android:textColor="@color/colorText"/> Viel Spaß! Aktiviere Begrüßungen Falls du möchtest, dass ich nett bin :) - Lizenzen + Lizenzen und Mitwirkende Was die App ausmacht (Englisch) Versionsnummer Datenschutzerklärung diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5023ea5..762bcd5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -39,8 +39,8 @@ Enable greeting If you want me to be nice :) - Licences - What it took to create this app + Licences and contributors + What went into making this app Version Number Terms & Conditions