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
import android.annotation.SuppressLint
import android.graphics.ImageDecoder
import android.graphics.drawable.AnimatedImageDrawable
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Button
import android.widget.EditText
import android.widget.ImageView
import android.widget.Toolbar
import androidx.annotation.RequiresApi
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONException
import org.json.JSONObject
import java.io.File
import java.io.IOException
import java.nio.charset.Charset
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
class MainActivity : AppCompatActivity(), MainActivityListener {
companion object {
val dayMap = mutableMapOf<Int, Day>()
var days = listOf<Day>()
}
private val accountDataFilePath: String = "account_data.txt"
private val client = OkHttpClient()
private val username = "USERNAME"
private val password = "PASSWORD"
private lateinit var username: String
private lateinit var password: String
private val apiBaseUrl = "https://aplikace.skolaonline.cz/solapi/api"
private val tokenUrl = "$apiBaseUrl/connect/token"
private var token = ""
private lateinit var token: String
private val userDataUrl = "$apiBaseUrl/v1/user"
private var userData = ""
private var studentId = ""
private lateinit var userData: String
private lateinit var studentId: String
private val timeTableUrl = "$apiBaseUrl/v1/timeTable"
//private val marksUrl = "$apiBaseUrl/v1/students/$studentId/marks/list"
private suspend fun setupAPI() {
return withContext(Dispatchers.IO) {
//Log.d("debug", "setting up API...")
token = getToken()
token = JSONObject(getToken()).get("access_token").toString()
if (token == "") {
return@withContext
}
userData = getUserDataJSON()
studentId = getStudentId()
//Log.d("debug", "API set up!")
}
}
@SuppressLint("MissingInflatedId")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash_screen)
setSupportActionBar(findViewById(R.id.toolbar))
showSplashScreen()
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
val animatedImageView: ImageView = findViewById(R.id.loading_gif)
@@ -83,63 +192,109 @@ class MainActivity : AppCompatActivity(), MainActivityListener {
animatedImageView.setImageDrawable(drawable)
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()
launch()
week = parseJSON(getTimeTableJSON())
launchPhase2(week)
}
}
// Override onCreateOptionsMenu to handle menu item clicks
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
// 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
}
private fun launchPhase2(week: List<Day>) {
days = week
for ((i, day) in week.withIndex()) {
//Log.d("debug", "$i, ${day.name}\n${day.date}\n${day.subjects}")
dayMap[i] = day
}
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() {
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())
days = week
for ((i, day) in week.withIndex()) {
//Log.d("debug", "$i, ${day.name}\n${day.date}\n${day.subjects}")
dayMap[i] = day
val accSaved: Boolean = haveSavedAccount()
//val valid: Boolean = accountIsValid(Account(username, password, 0))
//Log.d("debug", "username: $username")
//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 respString = resp.body?.string()
val respJson = respString?.let { JSONObject(it) }
val token = respJson?.get("access_token").toString()
token
respJson.toString()
} catch (e: JSONException) {
"""{"error": "json"}"""
} catch (e: Exception) {
//e.printStackTrace()
"""{"error": "network"}"""
}
}
}
private fun getStudentId(): String {
//Log.d("debug", userData)
return JSONObject(userData).get("personID").toString()
return if (!userIsParent()) {
JSONObject(userData).get("personID").toString()
} else {
JSONObject(userData).getJSONArray("children").getJSONObject(0).get("id").toString()
}
}
private suspend fun getUserDataJSON(): String {
@@ -187,21 +347,36 @@ class MainActivity : AppCompatActivity(), MainActivityListener {
try {
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) {
"""{"error": "network"}"""
}
}
}
private fun userIsParent(): Boolean {
return if (userData != "") JSONObject(userData).get("userType").toString() == "parent"
else false
}
private suspend fun getTimeTableJSON(): String {
// I now figured out how to use the SOL API (kinda), yay
//Log.d("debug", "getting timetable")
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()
.url(timeTableUrl)
.url(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Bearer $token")
.get()
@@ -209,16 +384,15 @@ class MainActivity : AppCompatActivity(), MainActivityListener {
try {
val resp = client.newCall(request).execute()
//Log.d("d", resp.body?.string().toString())
//Log.d("d", JSONObject(resp.body?.string().toString()).get("subject").toString())
resp.body?.string().toString()
val respString = resp.body?.string().toString()
//Log.d("debug", "timetable: $respString")
respString
} catch (e: IOException) {
"""{"error": "network"}"""
}
}
}
@RequiresApi(Build.VERSION_CODES.O)
private fun parseJSON(jsonString: String): List<Day> {
val jsonObject = JSONObject(jsonString)
@@ -231,7 +405,7 @@ class MainActivity : AppCompatActivity(), MainActivityListener {
val week = mutableListOf<Day>()
if (jsonObject.has("error")) {
Log.d("debug error", "network")
//Log.d("debug error", "network")
return week
}
//Log.d("debug", "no network error\n\n$jsonString")
@@ -10,8 +10,9 @@
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.Solen.AppBarOverlay"
android:fontFamily="@font/roboto_src">
android:layout_marginBottom="675dp"
android:fontFamily="@font/roboto_src"
android:theme="@style/Theme.Solen.AppBarOverlay">
<androidx.appcompat.widget.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"?>
<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
android:id="@+id/item_sign_in"
android:title="@string/sign_in" />
<item
android:id="@+id/item_sign_out"
android:title="@string/sign_out" />
<item
android:id="@+id/item_refresh"
android:icon="@android:drawable/ic_popup_sync"
android:title="@string/refresh" />
<item
android:id="@+id/item_settings"
android:icon="@android:drawable/ic_menu_manage"
android:title="@string/settings" />
</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="splash_image">splash image</string>
<string name="background">background</string>
<string name="settings">Settings</string>
<string name="sign_in">Sign In</string>
<string name="refresh">Refresh</string>
<string name="settings">Nastavení</string>
<string name="sign_in">Přihlásit se</string>
<string name="sign_out">Odhlásit se</string>
<string name="refresh">Znovu načíst data</string>
</resources>
+3 -1
View File
@@ -21,5 +21,7 @@
<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>