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:
Deniz Düzgören
2019-09-22 19:46:20 +02:00
parent 49eefe8131
commit 088aa90aa4
14 changed files with 163 additions and 54 deletions
@@ -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
+1
View File
@@ -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"
+1 -1
View File
@@ -46,6 +46,7 @@
<ImageView
android:id="@+id/iconView"
android:layout_width="wrap_content"
android:layout_marginEnd="4dp"
android:layout_height="20dp"
android:tint="@color/colorText"/>
@@ -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"/>
<TextView
+1 -1
View File
@@ -33,7 +33,7 @@
<string name="setup_successful">Viel Spaß!</string>
<string name="greeting_title">Aktiviere Begrüßungen</string>
<string name="greeting_desc">Falls du möchtest, dass ich nett bin :)</string>
<string name="licences_title">Lizenzen</string>
<string name="licences_title">Lizenzen und Mitwirkende</string>
<string name="licences_desc">Was die App ausmacht (Englisch)</string>
<string name="version">Versionsnummer</string>
<string name="privacy_policy_title">Datenschutzerklärung</string>
+2 -2
View File
@@ -39,8 +39,8 @@
<string name="greeting_title">Enable greeting</string>
<string name="greeting_desc">If you want me to be nice :)</string>
<string name="licences_title">Licences</string>
<string name="licences_desc">What it took to create this app</string>
<string name="licences_title">Licences and contributors</string>
<string name="licences_desc">What went into making this app</string>
<string name="version">Version Number</string>
<string name="terms_and_conditions_title">Terms &amp; Conditions</string>