diff --git a/app/src/main/java/com/denizk0461/studip/activity/FetcherActivity.kt b/app/src/main/java/com/denizk0461/studip/activity/FetcherActivity.kt index a34c7ec..2395237 100644 --- a/app/src/main/java/com/denizk0461/studip/activity/FetcherActivity.kt +++ b/app/src/main/java/com/denizk0461/studip/activity/FetcherActivity.kt @@ -14,9 +14,15 @@ import com.denizk0461.studip.databinding.ActivityFetcherBinding import com.denizk0461.studip.viewmodel.FetcherViewModel 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() { + // View binding private lateinit var binding: ActivityFetcherBinding + // View model private lateinit var viewModel: FetcherViewModel /* @@ -27,39 +33,51 @@ class FetcherActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // Instantiate view model viewModel = ViewModelProvider.AndroidViewModelFactory(this.application).create(FetcherViewModel::class.java) - + // Inflate view binding and bind to this activity binding = ActivityFetcherBinding.inflate(layoutInflater) setContentView(binding.root) + // Enabling JavaScript. See comment above lint suppression for reason why this is done. binding.webview.settings.apply { javaScriptEnabled = true } binding.webview.webViewClient = object : WebViewClient() { + // Disallow redirecting to prevent the app from launching Chrome after the user logs in override fun shouldOverrideUrlLoading( view: WebView, request: WebResourceRequest ): Boolean { return false } - override fun onPageFinished(view: WebView, url: String) { - binding.webview.loadUrl( - "javascript:window.HtmlViewer.showHTML" + - "(''+document.getElementsByTagName('html')[0].innerHTML+'');" - ) - } } + /* + * 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.fab.setOnClickListener { view -> + /* + * Retrieve encoded HTML via JavaScript function. Encoding is done as otherwise not all + * symbols will be accurately fetched. + */ binding.webview.evaluateJavascript( "(function(){return encodeURI(document.getElementsByTagName('html')[0].innerHTML)})();" ) { p0 -> + // Delete all previously fetched elements viewModel.nukeEvents() + // Decode HTML and parse it into a list of StudIPEvent.kt StudIPParser().parse(URLDecoder.decode(p0, "UTF-8")) { events -> + // Insert the list into the database 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() } } diff --git a/app/src/main/java/com/denizk0461/studip/activity/MainActivity.kt b/app/src/main/java/com/denizk0461/studip/activity/MainActivity.kt index 614a04a..297596b 100644 --- a/app/src/main/java/com/denizk0461/studip/activity/MainActivity.kt +++ b/app/src/main/java/com/denizk0461/studip/activity/MainActivity.kt @@ -3,9 +3,7 @@ package com.denizk0461.studip.activity import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat -import androidx.navigation.findNavController -import androidx.navigation.ui.AppBarConfiguration -import androidx.navigation.ui.navigateUp +import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction import androidx.lifecycle.Lifecycle 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.SettingsFragment +/** + * Main activity that handles all common fragments. This is opened on app launch. + */ class MainActivity : AppCompatActivity() { - private lateinit var appBarConfiguration: AppBarConfiguration + // View binding 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 = + listOf(EventFragment(), CanteenFragment(), SettingsFragment()) override fun onCreate(savedInstanceState: Bundle?) { WindowCompat.setDecorFitsSystemWindows(window, false) super.onCreate(savedInstanceState) + /* + * Instantiate repository object that is accessed by the fragments' view models to retrieve + * data. + */ Dependencies.repo = EventRepository(application) + // Inflate view binding and bind to this activity binding = ActivityMainBinding.inflate(layoutInflater) 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 -> - loadFragment(bottomIdToType(item.itemId)) + loadFragment(item.itemId) } + // Launch the fragment defined in 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) - val fragment = getFragment(type) + /** + * Loads a given fragment. + * + * @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) { + // Return false to avoid interrupting the fragment's lifecycle return false } -// if (supportFragmentManager.fragments.size > 1) { -// -// val currentFragment = supportFragmentManager.findFragmentByTag(currentFragment.type) -// if (currentFragment != null) { -// supportFragmentManager.beginTransaction().remove(currentFragment).commit() -// supportFragmentManager.popBackStack() -// } -// } - -// supportFragmentManager.popBackStack() + // Prepare and commit a transaction between the current and the next fragment 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) .commit() - binding.contentMain.appTitleBar.text = getTitleString(type) - - currentFragment = type + // Set text of the app bar accordingly + binding.contentMain.appTitleBar.text = getTitleString(id) + // Note down the new current fragment + currentFragment = id + // Transaction successful return true } -// override fun onCreateOptionsMenu(menu: Menu): Boolean { -// // Inflate the menu; this adds items to the action bar if it is present. -// menuInflater.inflate(R.menu.menu_main, menu) -// return true -// } - -// override fun onOptionsItemSelected(item: MenuItem): Boolean { -// // Handle action bar item clicks here. The action bar will -// // automatically handle clicks on the Home/Up button, so long -// // as you specify a parent activity in AndroidManifest.xml. -// 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() + /** + * Retrieve fragment by the ID of the bottom navigation view's button. + * + * @param id the ID of the bottom navigation button + * @return the corresponding fragment + */ + private fun getFragment(id: Int) = when (id) { + R.id.food -> fragments[1] // CanteenFragment.kt + R.id.settings -> fragments[2] // SettingsFragment.kt + else -> fragments[0] // on unknown value or R.id.plan, launch EventFragment.kt } -// private fun getFragmentByType(type: FragmentType): Fragment { -//// Log.d("AAA!", "help") -// return when (type) { -// FragmentType.SCHEDULE -> EventFragment() -// FragmentType.FOOD -> FoodFragment() -// FragmentType.SETTINGS -> SettingsFragment() -// } -// } - - private fun bottomIdToType(id: Int): FragmentType = when (id) { - 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"), + /** + * Retrieve app bar text by the ID of the bottom navigation view's button. + * + * @param id the ID of the bottom navigation button + * @return the corresponding fragment's title + */ + private fun getTitleString(id: Int) = when (id) { + R.id.food -> getString(R.string.title_food) + R.id.settings -> getString(R.string.title_settings) + else -> getString(R.string.title_schedule) // on unknown value or R.id.plan, get text for EventFragment.kt } } \ No newline at end of file diff --git a/app/src/main/java/com/denizk0461/studip/fragment/CanteenFragment.kt b/app/src/main/java/com/denizk0461/studip/fragment/CanteenFragment.kt index b510764..285b327 100644 --- a/app/src/main/java/com/denizk0461/studip/fragment/CanteenFragment.kt +++ b/app/src/main/java/com/denizk0461/studip/fragment/CanteenFragment.kt @@ -20,10 +20,9 @@ class CanteenFragment : Fragment() { // This property is only valid between onCreateView and // onDestroyView. + // BUG: if the fragment is changed before the refresh is finished, the app crashes with a NPE private val binding get() = _binding!! - private lateinit var liveData: LiveData> - private lateinit var viewPagerAdapter: CanteenOfferPageAdapter private val viewModel: CanteenViewModel by viewModels() diff --git a/app/src/main/java/com/denizk0461/studip/model/StudIPEvent.kt b/app/src/main/java/com/denizk0461/studip/model/StudIPEvent.kt index d6e8eb3..84e77fe 100644 --- a/app/src/main/java/com/denizk0461/studip/model/StudIPEvent.kt +++ b/app/src/main/java/com/denizk0461/studip/model/StudIPEvent.kt @@ -3,23 +3,40 @@ package com.denizk0461.studip.model import androidx.room.Entity 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") data class StudIPEvent( @PrimaryKey val id: Int, - val title: String, // the name of the event - val lecturer: String, // the lecturer(s) - val room: String, // the room the event takes place in - val day: Int, // 0 = Monday, 1 = Tuesday, 2 = Wednesday, 3 = Thursday, 4 = Friday - val timeslotStart: String, // the time the course starts - val timeslotEnd: String, // the time the course ends - val colour: String = "000000", // user-defined colour, TODO implement this + val title: String, + val lecturer: String, + val room: String, + val day: Int, + val timeslotStart: String, + val timeslotEnd: String, + val colour: String = "000000", ) { -// @Ignore private val timeSlotStartTimes: List = listOf("06:15", "08:15", "10:15", "12:15", "14:15", "16:15", "18:15", "20:15") -// @Ignore private val timeSlotEndTimes: List = 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" + // Auto-generated methods + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -39,6 +56,7 @@ data class StudIPEvent( result = 31 * result + day result = 31 * result + timeslotStart.hashCode() result = 31 * result + timeslotEnd.hashCode() + result = 31 * result + colour.hashCode() return result } }