Archived
Added further documentation, fixed slight spacing issue on course_card.xml when icon is set to R.drawable.ic_empty
This commit is contained in:
@@ -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<ConstraintLayout>(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
|
||||
|
||||
|
||||
@@ -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<TextView>(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 {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -130,6 +130,13 @@ internal class CardAdapter(private var mSubst: List<Subst>, 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)
|
||||
|
||||
@@ -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<Void, Void, Void>() {
|
||||
|
||||
|
||||
@@ -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<String>): 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<String>): Boolean {
|
||||
checking.forEach { check ->
|
||||
if (s.contains(check)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user