made signing in work with arbitrary accounts

This commit is contained in:
Odweta
2024-01-30 18:24:11 +01:00
parent 92d18abc91
commit 7b8f95b5b0
8 changed files with 340 additions and 85 deletions
@@ -1,75 +1,184 @@
package com.odweta.solen package com.odweta.solen
import android.annotation.SuppressLint
import android.graphics.ImageDecoder import android.graphics.ImageDecoder
import android.graphics.drawable.AnimatedImageDrawable import android.graphics.drawable.AnimatedImageDrawable
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.webkit.WebResourceError import android.widget.Button
import android.webkit.WebResourceRequest import android.widget.EditText
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.ImageView import android.widget.ImageView
import android.widget.Toolbar import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.File
import java.io.IOException import java.io.IOException
import java.nio.charset.Charset
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
class MainActivity : AppCompatActivity(), MainActivityListener { class MainActivity : AppCompatActivity(), MainActivityListener {
companion object { companion object {
val dayMap = mutableMapOf<Int, Day>() val dayMap = mutableMapOf<Int, Day>()
var days = listOf<Day>() var days = listOf<Day>()
} }
private val accountDataFilePath: String = "account_data.txt"
private val client = OkHttpClient() private val client = OkHttpClient()
private val username = "USERNAME" private lateinit var username: String
private val password = "PASSWORD" private lateinit var password: String
private val apiBaseUrl = "https://aplikace.skolaonline.cz/solapi/api" private val apiBaseUrl = "https://aplikace.skolaonline.cz/solapi/api"
private val tokenUrl = "$apiBaseUrl/connect/token" private val tokenUrl = "$apiBaseUrl/connect/token"
private var token = "" private lateinit var token: String
private val userDataUrl = "$apiBaseUrl/v1/user" private val userDataUrl = "$apiBaseUrl/v1/user"
private var userData = "" private lateinit var userData: String
private var studentId = "" private lateinit var studentId: String
private val timeTableUrl = "$apiBaseUrl/v1/timeTable" private val timeTableUrl = "$apiBaseUrl/v1/timeTable"
//private val marksUrl = "$apiBaseUrl/v1/students/$studentId/marks/list" //private val marksUrl = "$apiBaseUrl/v1/students/$studentId/marks/list"
private suspend fun setupAPI() { private suspend fun setupAPI() {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
//Log.d("debug", "setting up API...") //Log.d("debug", "setting up API...")
token = getToken() token = JSONObject(getToken()).get("access_token").toString()
if (token == "") { if (token == "") {
return@withContext return@withContext
} }
userData = getUserDataJSON() userData = getUserDataJSON()
studentId = getStudentId() studentId = getStudentId()
//Log.d("debug", "API set up!")
} }
} }
@SuppressLint("MissingInflatedId")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash_screen) showSplashScreen()
setSupportActionBar(findViewById(R.id.toolbar))
lifecycleScope.launch { lifecycleScope.launch {
launch()
}
}
// Override onCreateOptionsMenu to handle menu item clicks
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
private fun showSignInScreen() {
setContentView(R.layout.activity_sign_in)
setSignInButtonOnClick()
}
private fun showSettingsScreen() {
setContentView(R.layout.activity_settings)
findViewById<Button>(R.id.settingsBackButton).setOnClickListener {
launchPhase1()
}
}
private fun setSignInButtonOnClick() {
findViewById<Button>(R.id.signInButton).setOnClickListener {
onSignIn()
}
}
// Override onOptionsItemSelected to handle menu item clicks
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.item_refresh -> {
lifecycleScope.launch {
launch()
}
return true
}
R.id.item_sign_in -> {
showSignInScreen()
return true
}
R.id.item_settings -> {
showSettingsScreen()
return true
}
R.id.item_sign_out -> {
clearAccountFile()
showSignInScreen()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun clearAccountFile() {
val file = File(applicationContext.filesDir, accountDataFilePath)
file.delete()
file.createNewFile()
}
override fun onLaunch(view: View) {
launch()
}
private suspend fun accountIsValid(acc: Account): Boolean {
return withContext(Dispatchers.IO) {
username = acc.username
password = acc.password
val token = getToken()
//Log.d("debug", "token: '$token'")
!JSONObject(token).has("error")
}
}
private fun onSignIn() {
//Log.d("debug", "onSignIn called!")
val acc = Account(
findViewById<EditText>(R.id.usernameField).text.toString(),
findViewById<EditText>(R.id.passwordField).text.toString(),
0
)
showSplashScreen()
saveAccount(acc)
lifecycleScope.launch {
val valid = accountIsValid(acc)
//Log.d("debug", "account valid? $valid")
//Log.d("debug", acc.username)
//Log.d("debug", acc.password)
if (valid) {
launch()
} else {
showSignInScreen()
Toast.makeText(
applicationContext,
"Neplatné přihlašovací údaje, zkuste to znovu",
Toast.LENGTH_LONG
).show()
}
//Log.d("debug", "onSignIn finished!")
}
}
private fun showSplashScreen() {
lifecycleScope.launch {
setContentView(R.layout.activity_splash_screen)
// Assuming you have an ImageView with ID "animatedImageView" in your layout // Assuming you have an ImageView with ID "animatedImageView" in your layout
val animatedImageView: ImageView = findViewById(R.id.loading_gif) val animatedImageView: ImageView = findViewById(R.id.loading_gif)
@@ -83,63 +192,109 @@ class MainActivity : AppCompatActivity(), MainActivityListener {
animatedImageView.setImageDrawable(drawable) animatedImageView.setImageDrawable(drawable)
drawable.start() // Start the animation drawable.start() // Start the animation
} }
}
}
class Account(val username: String, val password: String, val error: Int)
private fun parseAccountDataFile(file: File): Account {
val lines: List<String> = file.readLines()
if (lines.size >= 2) {
if (lines[0] != "" && lines[1] != "") {
return Account(lines[0], lines[1], 0)
}
}
return Account("", "", 1)
}
private fun haveSavedAccount(): Boolean {
/* check if a file exists
* no -> create it and return false
* yes -> read the contents and use them as the username and password
*/
// check if file exists
val file = File(applicationContext.filesDir, accountDataFilePath)
return if (!file.exists()) {
// it does not exist
file.createNewFile()
false
} else {
//Log.d("debug", "account file found")
// it exists, so it shall be parsed
val acc: Account = parseAccountDataFile(file)
username = acc.username
password = acc.password
acc.error == 0
}
}
private fun saveAccount(acc: Account): Boolean {
return try {
val file = File(applicationContext.filesDir, accountDataFilePath)
file.writeText("${acc.username}\n${acc.password}\n", Charset.forName("UTF-8"))
true
} catch (e: Exception) {
false
}
}
private fun launchPhase1() {
var week: List<Day>
showSplashScreen()
lifecycleScope.launch {
setupAPI() setupAPI()
launch() week = parseJSON(getTimeTableJSON())
launchPhase2(week)
} }
} }
// Override onCreateOptionsMenu to handle menu item clicks private fun launchPhase2(week: List<Day>) {
override fun onCreateOptionsMenu(menu: Menu): Boolean { days = week
menuInflater.inflate(R.menu.menu_main, menu) for ((i, day) in week.withIndex()) {
return true //Log.d("debug", "$i, ${day.name}\n${day.date}\n${day.subjects}")
} dayMap[i] = day
// Override onOptionsItemSelected to handle menu item clicks
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.item_refresh -> {
lifecycleScope.launch {
launch()
}
return true
}
R.id.item_sign_in -> {
// TODO: IMPLEMENT
return true
}
R.id.item_settings -> {
// TODO: IMPLEMENT
return true
}
} }
return super.onOptionsItemSelected(item) setContentView(R.layout.activity_main)
val toolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
toolbar.title = ""
toolbar.inflateMenu(R.menu.menu_main)
val mainFragment = CalWeekFragment()
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, mainFragment)
.commit()
} }
@RequiresApi(Build.VERSION_CODES.O)
override fun onLaunch(view: View) {
launch()
}
@RequiresApi(Build.VERSION_CODES.O)
private fun launch() { private fun launch() {
lifecycleScope.launch { lifecycleScope.launch {
setContentView(R.layout.activity_loading_with_bar) /* check if there is a saved user account
* if yes, use that
* otherwise show the login activity
*/
val week = parseJSON(getTimeTableJSON()) val accSaved: Boolean = haveSavedAccount()
days = week //val valid: Boolean = accountIsValid(Account(username, password, 0))
for ((i, day) in week.withIndex()) {
//Log.d("debug", "$i, ${day.name}\n${day.date}\n${day.subjects}") //Log.d("debug", "username: $username")
dayMap[i] = day //Log.d("debug", "password: $password")
//Log.d("debug", "saved? $accSaved")//; valid? $valid")
if (!accSaved) {
//Log.d("debug", "showing sign in screen")
showSignInScreen()
} else {
//Log.d("debug", "launching timetable")
launchPhase1()
} }
setContentView(R.layout.activity_main)
val mainFragment = CalWeekFragment()
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer, mainFragment)
.commit()
} }
} }
@@ -163,17 +318,22 @@ class MainActivity : AppCompatActivity(), MainActivityListener {
val resp = client.newCall(request).execute() val resp = client.newCall(request).execute()
val respString = resp.body?.string() val respString = resp.body?.string()
val respJson = respString?.let { JSONObject(it) } val respJson = respString?.let { JSONObject(it) }
val token = respJson?.get("access_token").toString() respJson.toString()
token } catch (e: JSONException) {
"""{"error": "json"}"""
} catch (e: Exception) { } catch (e: Exception) {
//e.printStackTrace()
"""{"error": "network"}""" """{"error": "network"}"""
} }
} }
} }
private fun getStudentId(): String { private fun getStudentId(): String {
//Log.d("debug", userData) return if (!userIsParent()) {
return JSONObject(userData).get("personID").toString() JSONObject(userData).get("personID").toString()
} else {
JSONObject(userData).getJSONArray("children").getJSONObject(0).get("id").toString()
}
} }
private suspend fun getUserDataJSON(): String { private suspend fun getUserDataJSON(): String {
@@ -187,21 +347,36 @@ class MainActivity : AppCompatActivity(), MainActivityListener {
try { try {
val resp = client.newCall(request).execute() val resp = client.newCall(request).execute()
resp.body?.string().toString() val respString = resp.body?.string().toString()
//Log.d("debug", "userData: $respString")
respString
} catch (e: IOException) { } catch (e: IOException) {
"""{"error": "network"}""" """{"error": "network"}"""
} }
} }
} }
private fun userIsParent(): Boolean {
return if (userData != "") JSONObject(userData).get("userType").toString() == "parent"
else false
}
private suspend fun getTimeTableJSON(): String { private suspend fun getTimeTableJSON(): String {
// I now figured out how to use the SOL API (kinda), yay // I now figured out how to use the SOL API (kinda), yay
//Log.d("debug", "getting timetable") //Log.d("debug", "getting timetable")
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val userIsParent = userIsParent()
//Log.d("debug", "userIsParent? $userIsParent")
var url = timeTableUrl
if (userIsParent) {
url += "?studentId=$studentId"
}
//Log.d("debug", "url: $url")
val request = Request.Builder() val request = Request.Builder()
.url(timeTableUrl) .url(url)
.header("Content-Type", "application/x-www-form-urlencoded") .header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Bearer $token") .header("Authorization", "Bearer $token")
.get() .get()
@@ -209,16 +384,15 @@ class MainActivity : AppCompatActivity(), MainActivityListener {
try { try {
val resp = client.newCall(request).execute() val resp = client.newCall(request).execute()
//Log.d("d", resp.body?.string().toString()) val respString = resp.body?.string().toString()
//Log.d("d", JSONObject(resp.body?.string().toString()).get("subject").toString()) //Log.d("debug", "timetable: $respString")
resp.body?.string().toString() respString
} catch (e: IOException) { } catch (e: IOException) {
"""{"error": "network"}""" """{"error": "network"}"""
} }
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
private fun parseJSON(jsonString: String): List<Day> { private fun parseJSON(jsonString: String): List<Day> {
val jsonObject = JSONObject(jsonString) val jsonObject = JSONObject(jsonString)
@@ -231,7 +405,7 @@ class MainActivity : AppCompatActivity(), MainActivityListener {
val week = mutableListOf<Day>() val week = mutableListOf<Day>()
if (jsonObject.has("error")) { if (jsonObject.has("error")) {
Log.d("debug error", "network") //Log.d("debug error", "network")
return week return week
} }
//Log.d("debug", "no network error\n\n$jsonString") //Log.d("debug", "no network error\n\n$jsonString")
@@ -10,8 +10,9 @@
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="@style/Theme.Solen.AppBarOverlay" android:layout_marginBottom="675dp"
android:fontFamily="@font/roboto_src"> android:fontFamily="@font/roboto_src"
android:theme="@style/Theme.Solen.AppBarOverlay">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/settingsBackButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="315dp"
android:layout_marginBottom="675dp"
android:text="Zpět"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/usernameField"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:ems="10"
android:hint="Uživatelské jméno"
android:inputType="text"
app:layout_constraintBottom_toTopOf="@+id/passwordField"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/passwordField"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:ems="10"
android:hint="Heslo"
android:inputType="textPassword"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/usernameField" />
<Button
android:id="@+id/signInButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="137dp"
android:layout_marginTop="72dp"
android:layout_marginEnd="137dp"
android:layout_marginBottom="88dp"
android:text="Přihlásit se"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/passwordField" />
</androidx.constraintlayout.widget.ConstraintLayout>
+7 -4
View File
@@ -1,14 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/item_settings"
android:icon="@android:drawable/ic_menu_manage"
android:title="@string/settings" />
<item <item
android:id="@+id/item_sign_in" android:id="@+id/item_sign_in"
android:title="@string/sign_in" /> android:title="@string/sign_in" />
<item
android:id="@+id/item_sign_out"
android:title="@string/sign_out" />
<item <item
android:id="@+id/item_refresh" android:id="@+id/item_refresh"
android:icon="@android:drawable/ic_popup_sync" android:icon="@android:drawable/ic_popup_sync"
android:title="@string/refresh" /> android:title="@string/refresh" />
<item
android:id="@+id/item_settings"
android:icon="@android:drawable/ic_menu_manage"
android:title="@string/settings" />
</menu> </menu>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+4 -3
View File
@@ -5,7 +5,8 @@
<string name="kolaonline_enhanced">ŠkolaOnLine Enhanced</string> <string name="kolaonline_enhanced">ŠkolaOnLine Enhanced</string>
<string name="splash_image">splash image</string> <string name="splash_image">splash image</string>
<string name="background">background</string> <string name="background">background</string>
<string name="settings">Settings</string> <string name="settings">Nastavení</string>
<string name="sign_in">Sign In</string> <string name="sign_in">Přihlásit se</string>
<string name="refresh">Refresh</string> <string name="sign_out">Odhlásit se</string>
<string name="refresh">Znovu načíst data</string>
</resources> </resources>
+3 -1
View File
@@ -21,5 +21,7 @@
<style name="Theme.Solen.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" /> <style name="Theme.Solen.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="Theme.Solen.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" /> <style name="Theme.Solen.PopupOverlay" parent="ThemeOverlay.AppCompat.Light">
<item name="android:textColor">@color/white</item>
</style>
</resources> </resources>