Added comments and cleaned up code in activities and StudIPEvent.kt

This commit is contained in:
denizk0461
2023-04-21 15:42:21 +02:00
parent 7a6926ff64
commit 2534849329
4 changed files with 117 additions and 108 deletions
@@ -14,9 +14,15 @@ import com.denizk0461.studip.databinding.ActivityFetcherBinding
import com.denizk0461.studip.viewmodel.FetcherViewModel import com.denizk0461.studip.viewmodel.FetcherViewModel
import java.net.URLDecoder import java.net.URLDecoder
/**
* Activity for allowing the user to log in to access and fetch their schedule from their Stud.IP
* profile. Launched through a button in the app's settings.
*/
class FetcherActivity : Activity() { class FetcherActivity : Activity() {
// View binding
private lateinit var binding: ActivityFetcherBinding private lateinit var binding: ActivityFetcherBinding
// View model
private lateinit var viewModel: FetcherViewModel private lateinit var viewModel: FetcherViewModel
/* /*
@@ -27,39 +33,51 @@ class FetcherActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Instantiate view model
viewModel = ViewModelProvider.AndroidViewModelFactory(this.application).create(FetcherViewModel::class.java) viewModel = ViewModelProvider.AndroidViewModelFactory(this.application).create(FetcherViewModel::class.java)
// Inflate view binding and bind to this activity
binding = ActivityFetcherBinding.inflate(layoutInflater) binding = ActivityFetcherBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
// Enabling JavaScript. See comment above lint suppression for reason why this is done.
binding.webview.settings.apply { binding.webview.settings.apply {
javaScriptEnabled = true javaScriptEnabled = true
} }
binding.webview.webViewClient = object : WebViewClient() { binding.webview.webViewClient = object : WebViewClient() {
// Disallow redirecting to prevent the app from launching Chrome after the user logs in
override fun shouldOverrideUrlLoading( override fun shouldOverrideUrlLoading(
view: WebView, view: WebView,
request: WebResourceRequest request: WebResourceRequest
): Boolean { ): Boolean {
return false return false
} }
override fun onPageFinished(view: WebView, url: String) {
binding.webview.loadUrl(
"javascript:window.HtmlViewer.showHTML" +
"('<html>'+document.getElementsByTagName('html')[0].innerHTML+'</html>');"
)
}
} }
/*
* Attempt to load the user-set Stud.IP front page. Redirects to login page if user is
* not logged in.
*/
binding.webview.loadUrl("https://elearning.uni-bremen.de/index.php?again=yes") binding.webview.loadUrl("https://elearning.uni-bremen.de/index.php?again=yes")
binding.fab.setOnClickListener { view -> binding.fab.setOnClickListener { view ->
/*
* Retrieve encoded HTML via JavaScript function. Encoding is done as otherwise not all
* symbols will be accurately fetched.
*/
binding.webview.evaluateJavascript( binding.webview.evaluateJavascript(
"(function(){return encodeURI(document.getElementsByTagName('html')[0].innerHTML)})();" "(function(){return encodeURI(document.getElementsByTagName('html')[0].innerHTML)})();"
) { p0 -> ) { p0 ->
// Delete all previously fetched elements
viewModel.nukeEvents() viewModel.nukeEvents()
// Decode HTML and parse it into a list of StudIPEvent.kt
StudIPParser().parse(URLDecoder.decode(p0, "UTF-8")) { events -> StudIPParser().parse(URLDecoder.decode(p0, "UTF-8")) { events ->
// Insert the list into the database
viewModel.insertEvents(events) viewModel.insertEvents(events)
Toast.makeText(this, R.string.toast_fetch_finished, Toast.LENGTH_SHORT).show() // Notify the user that the fetch was successful. TODO: notify on error
Toast.makeText(
this, R.string.toast_fetch_finished, Toast.LENGTH_SHORT
).show()
// Close the activity
finish() finish()
} }
} }
@@ -3,9 +3,7 @@ package com.denizk0461.studip.activity
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.navigation.findNavController import androidx.fragment.app.Fragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import com.denizk0461.studip.R import com.denizk0461.studip.R
@@ -16,124 +14,100 @@ import com.denizk0461.studip.fragment.EventFragment
import com.denizk0461.studip.fragment.CanteenFragment import com.denizk0461.studip.fragment.CanteenFragment
import com.denizk0461.studip.fragment.SettingsFragment import com.denizk0461.studip.fragment.SettingsFragment
/**
* Main activity that handles all common fragments. This is opened on app launch.
*/
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration // View binding
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private var currentFragment = FragmentType.SCHEDULE /*
private val fragments = listOf(EventFragment(), CanteenFragment(), SettingsFragment()) * ID of the bottom navigation view button that launched the currently active fragment; set on
* activity launch to the fragment that will be first shown
*/
private var currentFragment: Int = R.id.plan
// All fragments are instantiated at app launch
private val fragments: List<Fragment> =
listOf(EventFragment(), CanteenFragment(), SettingsFragment())
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
/*
* Instantiate repository object that is accessed by the fragments' view models to retrieve
* data.
*/
Dependencies.repo = EventRepository(application) Dependencies.repo = EventRepository(application)
// Inflate view binding and bind to this activity
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
/*
* Set up navigation view bar to launch fragments.
* TODO: why do fragments launch slowly when quickly clicking between them?
*/
binding.contentMain.navView.setOnItemSelectedListener { item -> binding.contentMain.navView.setOnItemSelectedListener { item ->
loadFragment(bottomIdToType(item.itemId)) loadFragment(item.itemId)
} }
// Launch the fragment defined in currentFragment
loadFragment(currentFragment) loadFragment(currentFragment)
// setSupportActionBar(binding.toolbar)
// val navController = findNavController(R.id.nav_host_fragment_content_main)
// appBarConfiguration = AppBarConfiguration(navController.graph)
// setupActionBarWithNavController(navController, appBarConfiguration)
// binding.fab.setOnClickListener { view ->
// findNavController(R.id.nav_host_fragment_content_main).navigate(R.id.action_FirstFragment_to_SecondFragment)
//// Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
//// .setAnchorView(R.id.fab)
//// .setAction("Action", null).show()
// }
} }
private fun loadFragment(type: FragmentType): Boolean { /**
// val fragment = supportFragmentManager.findFragmentByTag(type.type) ?: getFragmentByType(type) * Loads a given fragment.
val fragment = getFragment(type) *
* @param id the ID of the bottom navigation view button that was clicked
* @return true on success, false if another fragment is currently instantiating
*/
private fun loadFragment(id: Int): Boolean {
// Get fragment to be loaded
val fragment = getFragment(id)
// Check if another fragment exists and is not fully initialised
if (fragment.lifecycle.currentState != Lifecycle.State.INITIALIZED) { if (fragment.lifecycle.currentState != Lifecycle.State.INITIALIZED) {
// Return false to avoid interrupting the fragment's lifecycle
return false return false
} }
// if (supportFragmentManager.fragments.size > 1) { // Prepare and commit a transaction between the current and the next fragment
//
// val currentFragment = supportFragmentManager.findFragmentByTag(currentFragment.type)
// if (currentFragment != null) {
// supportFragmentManager.beginTransaction().remove(currentFragment).commit()
// supportFragmentManager.popBackStack()
// }
// }
// supportFragmentManager.popBackStack()
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.nav_host_fragment_content_main, fragment, type.type) .replace(R.id.nav_host_fragment_content_main, fragment, id.toString())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.commit() .commit()
binding.contentMain.appTitleBar.text = getTitleString(type) // Set text of the app bar accordingly
binding.contentMain.appTitleBar.text = getTitleString(id)
currentFragment = type // Note down the new current fragment
currentFragment = id
// Transaction successful
return true return true
} }
// override fun onCreateOptionsMenu(menu: Menu): Boolean { /**
// // Inflate the menu; this adds items to the action bar if it is present. * Retrieve fragment by the ID of the bottom navigation view's button.
// menuInflater.inflate(R.menu.menu_main, menu) *
// return true * @param id the ID of the bottom navigation button
// } * @return the corresponding fragment
*/
// override fun onOptionsItemSelected(item: MenuItem): Boolean { private fun getFragment(id: Int) = when (id) {
// // Handle action bar item clicks here. The action bar will R.id.food -> fragments[1] // CanteenFragment.kt
// // automatically handle clicks on the Home/Up button, so long R.id.settings -> fragments[2] // SettingsFragment.kt
// // as you specify a parent activity in AndroidManifest.xml. else -> fragments[0] // on unknown value or R.id.plan, launch EventFragment.kt
// return when (item.itemId) {
// R.id.action_settings -> true
// else -> super.onOptionsItemSelected(item)
// }
// }
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_content_main)
return navController.navigateUp(appBarConfiguration)
|| super.onSupportNavigateUp()
} }
// private fun getFragmentByType(type: FragmentType): Fragment { /**
//// Log.d("AAA!", "help") * Retrieve app bar text by the ID of the bottom navigation view's button.
// return when (type) { *
// FragmentType.SCHEDULE -> EventFragment() * @param id the ID of the bottom navigation button
// FragmentType.FOOD -> FoodFragment() * @return the corresponding fragment's title
// FragmentType.SETTINGS -> SettingsFragment() */
// } private fun getTitleString(id: Int) = when (id) {
// } R.id.food -> getString(R.string.title_food)
R.id.settings -> getString(R.string.title_settings)
private fun bottomIdToType(id: Int): FragmentType = when (id) { else -> getString(R.string.title_schedule) // on unknown value or R.id.plan, get text for EventFragment.kt
R.id.food -> FragmentType.FOOD
R.id.settings -> FragmentType.SETTINGS
else -> FragmentType.SCHEDULE
}
private fun getFragment(type: FragmentType) = when (type) {
FragmentType.SCHEDULE -> fragments[0]
FragmentType.FOOD -> fragments[1]
FragmentType.SETTINGS -> fragments[2]
}
private fun getTitleString(type: FragmentType) = when (type) {
FragmentType.SCHEDULE -> getString(R.string.title_schedule)
FragmentType.FOOD -> getString(R.string.title_food)
FragmentType.SETTINGS -> getString(R.string.title_settings)
}
enum class FragmentType(val type: String) {
SCHEDULE("schedule"),
FOOD("food"),
SETTINGS("settings"),
} }
} }
@@ -20,10 +20,9 @@ class CanteenFragment : Fragment() {
// This property is only valid between onCreateView and // This property is only valid between onCreateView and
// onDestroyView. // onDestroyView.
// BUG: if the fragment is changed before the refresh is finished, the app crashes with a NPE
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var liveData: LiveData<List<CanteenOffer>>
private lateinit var viewPagerAdapter: CanteenOfferPageAdapter private lateinit var viewPagerAdapter: CanteenOfferPageAdapter
private val viewModel: CanteenViewModel by viewModels() private val viewModel: CanteenViewModel by viewModels()
@@ -3,23 +3,40 @@ package com.denizk0461.studip.model
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
/**
* Entity for storing entries of the user's Stud.IP schedule. Registered in the app database.
*
* @param id primary key that uniquely identifies the entry
* @param title title of the event
* @param lecturer lecturer(s) organising the event
* @param room room the event takes place in
* @param day day the event takes place on; 0 = Monday, 4 = Friday
* @param timeslotStart time the event starts at
* @param timeslotEnd time the event ends at
* @param colour user-defined colour the event will be shown in - UNIMPLEMENTED
*/
@Entity(tableName = "events") @Entity(tableName = "events")
data class StudIPEvent( data class StudIPEvent(
@PrimaryKey val id: Int, @PrimaryKey val id: Int,
val title: String, // the name of the event val title: String,
val lecturer: String, // the lecturer(s) val lecturer: String,
val room: String, // the room the event takes place in val room: String,
val day: Int, // 0 = Monday, 1 = Tuesday, 2 = Wednesday, 3 = Thursday, 4 = Friday val day: Int,
val timeslotStart: String, // the time the course starts val timeslotStart: String,
val timeslotEnd: String, // the time the course ends val timeslotEnd: String,
val colour: String = "000000", // user-defined colour, TODO implement this val colour: String = "000000",
) { ) {
// @Ignore private val timeSlotStartTimes: List<String> = listOf("06:15", "08:15", "10:15", "12:15", "14:15", "16:15", "18:15", "20:15") /**
// @Ignore private val timeSlotEndTimes: List<String> = listOf("07:45", "09:45", "11:45", "13:45", "15:45", "17:45", "19:45", "21:45") * Parse timeslotStart and timeslotEnd into an easily readable string in the following format:
* 12:15 13:45
*
* @return formatted time string
*/
fun timeslot(): String = "$timeslotStart $timeslotEnd" fun timeslot(): String = "$timeslotStart $timeslotEnd"
// Auto-generated methods
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
@@ -39,6 +56,7 @@ data class StudIPEvent(
result = 31 * result + day result = 31 * result + day
result = 31 * result + timeslotStart.hashCode() result = 31 * result + timeslotStart.hashCode()
result = 31 * result + timeslotEnd.hashCode() result = 31 * result + timeslotEnd.hashCode()
result = 31 * result + colour.hashCode()
return result return result
} }
} }