diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index 0c0c338..0000000 --- a/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f0fdb85..5dd62a3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -61,11 +61,13 @@ dependencies { implementation("androidx.datastore:datastore-preferences:1.0.0") implementation("com.google.code.gson:gson:2.9.0") implementation(platform("androidx.compose:compose-bom:2023.03.00")) + implementation(platform("androidx.compose:compose-bom:2023.03.00")) testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.compose.ui:ui-test-junit4") androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00")) + androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00")) debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-test-manifest") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6071109..912e310 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,11 @@ android:supportsRtl="true" android:theme="@style/Theme.CleverClass" tools:targetApi="31"> + { - DSBLoading() + -2 -> { + LoadingScreen() } - -1 -> { - DSBNoInternet() + NO_INTERNET_CONNECTION_CODE -> { + DSBError("Keine Internetverbindung") } - else -> { + 1 ->{ + DSBError("Login fehlgeschlagen") + } + 0 -> { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier @@ -137,9 +160,7 @@ fun DSBContent(activity: ComponentActivity){ ) { Spacer(modifier = Modifier.height(32.dp)) - val mainFolder = File(activity.filesDir, "dataDSB") - - mainFolder.listFiles()?.forEach { folder -> + folders.forEach { folder -> DayPrefab(folder, activity) Spacer(modifier = Modifier.height(32.dp)) Divider() @@ -147,28 +168,43 @@ fun DSBContent(activity: ComponentActivity){ } } } + else -> { + DSBError("Leider ist ein Problem aufgetreten\nFehlercode: $loadingState") + } + } + } + + // changes state when Shared Preference is updated + val observer = remember { + SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key == "password" || key == "username") { + loadingState = -2 + + GlobalScope.launch(Dispatchers.IO) { + loadingState = downloadDSB(activity) + folders = mainFolder.listFiles() + } + } + } + } + + // handles observer disposal + DisposableEffect(Unit) { + sharedPreferences.registerOnSharedPreferenceChangeListener(observer) + onDispose { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(observer) } } } @Composable -fun DSBLoading(){ - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ){ - CircularProgressIndicator() - } -} - -@Composable -fun DSBNoInternet(){ +fun DSBError(message: String){ Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ){ Text( - text = "Keine Internetverbindung", + text = message, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onBackground ) diff --git a/app/src/main/java/com/schoolapp/cleverclass/DSBDayViewActivity.kt b/app/src/main/java/com/schoolapp/cleverclass/DSBDayViewActivity.kt index 9e1d0db..12858df 100644 --- a/app/src/main/java/com/schoolapp/cleverclass/DSBDayViewActivity.kt +++ b/app/src/main/java/com/schoolapp/cleverclass/DSBDayViewActivity.kt @@ -69,7 +69,7 @@ fun DSBDayViewContent(activity: ComponentActivity){ colors = TopAppBarDefaults.centerAlignedTopAppBarColors(MaterialTheme.colorScheme.primaryContainer), title = { Text( - text = information.get("PlanInfo").toString(), + text = information.get("PlanInfo").toString().removeSuffix(".pdf"), style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onPrimaryContainer )}, diff --git a/app/src/main/java/com/schoolapp/cleverclass/DSBDownloader.kt b/app/src/main/java/com/schoolapp/cleverclass/DSBDownloader.kt index 7e2d571..9a012f5 100644 --- a/app/src/main/java/com/schoolapp/cleverclass/DSBDownloader.kt +++ b/app/src/main/java/com/schoolapp/cleverclass/DSBDownloader.kt @@ -14,26 +14,31 @@ import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream import java.io.ByteArrayInputStream import java.io.FileOutputStream +import java.net.SocketException import java.time.LocalDateTime import java.util.UUID suspend fun downloadDSB(context: Context): Int{ + val sharedPreferences = context.getSharedPreferences("DSBmobile", Context.MODE_PRIVATE) + val username = sharedPreferences.getString("username", "").toString() + val password = sharedPreferences.getString("password", "").toString() + if(!isInternetAvailable(context)) - return -1 + return NO_INTERNET_CONNECTION_CODE val mainDir = createDataFolder(context) - val jsonResponse = downloadDataTask() + val jsonResponse = JSONObject(downloadDataTask(username = username, password = password)) + if(jsonResponse.get("Resultcode") != 0) + return jsonResponse.getInt("Resultcode") - downloadImagesTask(jsonResponse, mainDir) - - return 0 + return downloadImagesTask(jsonResponse, mainDir) } -private fun downloadDataTask() : String { +private fun downloadDataTask(username: String, password: String) : String { return try { - val requestBody = createRequestBody() + val requestBody = createRequestBody(username = username, password = password) val jsonRequest = createJsonRequest(requestBody) val responseData = fetchData(jsonRequest) @@ -45,9 +50,7 @@ private fun downloadDataTask() : String { } } -private fun createRequestBody(): String { - val username = "149002" - val password = "Vertretungsplan" +private fun createRequestBody(username: String, password: String): String { val currentDateTime = LocalDateTime.now().toString() val requestBody = JSONObject().apply { @@ -118,11 +121,10 @@ private fun decompressGZIPAndDecodeBase64(compressedData: String): String { return outputStream.toString("UTF-8") } -private fun downloadImagesTask(jsonResponse: String, mainDir : File){ +private fun downloadImagesTask(jsonResponse: JSONObject, mainDir : File) : Int { clearFolder(mainDir) - val jsonResponseObject = JSONObject(jsonResponse) - val jsonDaysObject = jsonResponseObject.getJSONArray("ResultMenuItems").getJSONObject(0).getJSONArray("Childs").getJSONObject(0).getJSONObject("Root").getJSONArray("Childs") + val jsonDaysObject = jsonResponse.getJSONArray("ResultMenuItems").getJSONObject(0).getJSONArray("Childs").getJSONObject(0).getJSONObject("Root").getJSONArray("Childs") for (d in 0 until jsonDaysObject.length()) { val jsonDayObject = jsonDaysObject.getJSONObject(d) @@ -139,9 +141,12 @@ private fun downloadImagesTask(jsonResponse: String, mainDir : File){ val pageIndex = jsonPageObject.get("Index") val imageLink = jsonPageObject.get("Detail").toString() - downloadImage(day, URL(imageLink), "$pageIndex.jpg") + val downloadState = downloadImage(day, URL(imageLink), "$pageIndex.jpg") + if (downloadState != 0) + return downloadState } } + return 0 } private fun createDayInformationJson(folder: File, jsonDayObject: JSONObject){ @@ -158,22 +163,29 @@ private fun createDayInformationJson(folder: File, jsonDayObject: JSONObject){ infoFile.writeText(infoJson.toString()) } -private fun downloadImage(folder: File, url: URL, fileName: String){ +private fun downloadImage(folder: File, url: URL, fileName: String) : Int { val file = File(folder, fileName) - val connection = url.openConnection() - val inputStream = connection.getInputStream() - val outputStream = FileOutputStream(file) + try { + val connection = url.openConnection() + val inputStream = connection.getInputStream() + val outputStream = FileOutputStream(file) - val buffer = ByteArray(1024) - var bytesRead: Int + val buffer = ByteArray(1024) + var bytesRead: Int - while (inputStream.read(buffer).also { bytesRead = it } != -1) { - outputStream.write(buffer, 0, bytesRead) + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + outputStream.write(buffer, 0, bytesRead) + } + + inputStream.close() + outputStream.close() } - - inputStream.close() - outputStream.close() + catch (e: SocketException){ + Log.d("ImageDownloader", e.toString()) + return NO_INTERNET_CONNECTION_CODE + } + return 0 } private fun createDataFolder(context: Context) : File { diff --git a/app/src/main/java/com/schoolapp/cleverclass/DSBLoginActivity.kt b/app/src/main/java/com/schoolapp/cleverclass/DSBLoginActivity.kt new file mode 100644 index 0000000..53557e5 --- /dev/null +++ b/app/src/main/java/com/schoolapp/cleverclass/DSBLoginActivity.kt @@ -0,0 +1,316 @@ +package com.schoolapp.cleverclass + +import android.content.Context +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.selection.TextSelectionColors +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import com.schoolapp.cleverclass.ui.theme.CleverClassTheme +import com.schoolapp.cleverclass.ui.theme.InputPrimaryColor +import com.schoolapp.cleverclass.ui.theme.InputSecondaryColor + +private lateinit var activityType : String + +class DSBLoginActivity : ComponentActivity() { + companion object{ + const val TYPE_KEY = "type_key" + } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + activityType = intent.getStringExtra(TYPE_KEY).toString() + + setContent { + CleverClassTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + DSBLoginContent(activity = this) + } + } + } + } +} + + +// Content of DSBLogin +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DSBLoginContent(activity: ComponentActivity){ + val inputFieldColors = TextFieldDefaults.outlinedTextFieldColors( + textColor = MaterialTheme.colorScheme.onPrimaryContainer, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + cursorColor = InputPrimaryColor, + selectionColors = TextSelectionColors( + handleColor = InputPrimaryColor, + backgroundColor = InputSecondaryColor + ), + focusedBorderColor = InputSecondaryColor, + unfocusedBorderColor = Color.Transparent + ) + + val sharedPreferences = activity.getSharedPreferences("DSBmobile", Context.MODE_PRIVATE) + val editor = sharedPreferences.edit() + + var username by remember { + mutableStateOf(sharedPreferences.getString("username", "").toString()) + } + var password by remember { + mutableStateOf(sharedPreferences.getString("password", "").toString()) + } + var showPassword by remember { + mutableStateOf(false) + } + + var showInfo by remember { + mutableStateOf(false) + } + var showDeleteConfirmation by remember { + mutableStateOf(false) + } + + Column { + TopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors(MaterialTheme.colorScheme.primaryContainer), + title = { Text(text = "") }, + navigationIcon = { + IconButton(onClick = { activity.finish() }) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = null, + modifier = Modifier.size(28.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + }, + modifier = Modifier.fillMaxWidth() + ) + + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxHeight() + .verticalScroll(rememberScrollState()) + ) { + Surface( + color = MaterialTheme.colorScheme.primaryContainer, + shape = RoundedCornerShape(20), + modifier = Modifier.padding(16.dp) + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(16.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "DSBmobile Login Daten", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.padding(10.dp) + ) + + Spacer(modifier = Modifier.weight(1f)) + + if (activityType == "Settings") + IconButton(onClick = { showDeleteConfirmation = !showDeleteConfirmation }) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = null, + modifier = Modifier.size(28.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + else if (activityType == "FirstTime") + IconButton(onClick = { showInfo = !showInfo }) { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + OutlinedTextField( + value = username, + onValueChange = { username = it }, + placeholder = { + Text( + text = "Benutzername", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primaryContainer + ) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(20), + textStyle = MaterialTheme.typography.labelMedium, + colors = inputFieldColors + ) + + Spacer(modifier = Modifier.height(16.dp)) + + OutlinedTextField( + value = password, + onValueChange = { password = it }, + placeholder = { + Text( + text = "Passwort", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primaryContainer + ) + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(20), + textStyle = MaterialTheme.typography.labelMedium, + colors = inputFieldColors, + visualTransformation = if(showPassword) VisualTransformation.None else PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + trailingIcon = { + IconButton(onClick = { showPassword = !showPassword }) + { + Image( + painter = painterResource( + id = if(showPassword) + R.drawable.baseline_visibility_24 + else + R.drawable.baseline_visibility_off_24 + ), + contentDescription = null, + ) + } + } + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.clickable { + editor.putString("username", username) + editor.putString("password", password) + editor.apply() + activity.finish() + } + ) { + Text( + text = "Speichen", + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(10.dp) + ) + } + } + } + } + } + + if (showInfo) { + AlertDialog( + onDismissRequest = { showInfo = false }, + text = { + Text( + text = "Der Benutzername und das Passwort können später in den Einstellungen geändert werden", + color = MaterialTheme.colorScheme.onPrimaryContainer, + style = MaterialTheme.typography.labelMedium) + }, + confirmButton = { + Text( + text = "Schließen", + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.clickable { showInfo = false } + ) + }, + containerColor = MaterialTheme.colorScheme.primaryContainer, + modifier = Modifier + .padding(5.dp) + ) + } + + if (showDeleteConfirmation) { + AlertDialog( + onDismissRequest = { showDeleteConfirmation = false }, + text = { + Text( + text = "Möchten Sie wirklich die Anmeldedaten löschen?", + color = MaterialTheme.colorScheme.onPrimaryContainer, + style = MaterialTheme.typography.labelMedium + ) + }, + confirmButton = { + Row(modifier = Modifier.fillMaxWidth()){ + Text( + text = "Bestätigen", + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.clickable { + editor.putString("username", "") + editor.putString("password", "") + editor.apply() + showDeleteConfirmation = false + activity.finish() + } + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = "Abbrechen", + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.clickable { showDeleteConfirmation = false } + ) + } + }, + containerColor = MaterialTheme.colorScheme.primaryContainer + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/schoolapp/cleverclass/NotenActivity.kt b/app/src/main/java/com/schoolapp/cleverclass/NotenActivity.kt index 0c99ee9..15f44b2 100644 --- a/app/src/main/java/com/schoolapp/cleverclass/NotenActivity.kt +++ b/app/src/main/java/com/schoolapp/cleverclass/NotenActivity.kt @@ -323,7 +323,7 @@ fun FachButton( } } - // handles observer creation and disposal + // handles observer disposal DisposableEffect(Unit) { sharedPreferences.registerOnSharedPreferenceChangeListener(observer) onDispose { diff --git a/app/src/main/java/com/schoolapp/cleverclass/SettingsActivity.kt b/app/src/main/java/com/schoolapp/cleverclass/SettingsActivity.kt index 46aa53d..e57cabb 100644 --- a/app/src/main/java/com/schoolapp/cleverclass/SettingsActivity.kt +++ b/app/src/main/java/com/schoolapp/cleverclass/SettingsActivity.kt @@ -8,12 +8,14 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight @@ -98,6 +100,25 @@ fun SettingsContent(activity: ComponentActivity) { Setting(text = "Periodensystem", sharedPreferences, editor) Setting(text = "Mebis", sharedPreferences, editor) Setting(text = "DSBmobile", sharedPreferences, editor) + Spacer(modifier = Modifier.height(16.dp)) + Divider() + Box( + modifier = Modifier + .fillMaxWidth() + .clickable { + val intent = Intent(activity, DSBLoginActivity::class.java).apply { + putExtra(DSBLoginActivity.TYPE_KEY, "Settings") + } + activity.startActivity(intent) + } + ) { + Text( + text = "DSBmobile Daten", + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(16.dp) + ) + } } // About Button @@ -123,7 +144,6 @@ fun SettingsContent(activity: ComponentActivity) { } } - @Composable fun Setting(text : String, sharedPreferences: SharedPreferences, editor: Editor){ val checkedState = remember { mutableStateOf(sharedPreferences.getBoolean(text, true)) } @@ -139,8 +159,10 @@ fun Setting(text : String, sharedPreferences: SharedPreferences, editor: Editor) } ) Spacer(modifier = Modifier.width(16.dp)) - Text(text = text, + Text( + text = text, color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.labelMedium) + style = MaterialTheme.typography.labelMedium + ) } } diff --git a/app/src/main/java/com/schoolapp/cleverclass/Utilities.kt b/app/src/main/java/com/schoolapp/cleverclass/Utilities.kt index a8d2e04..f191763 100644 --- a/app/src/main/java/com/schoolapp/cleverclass/Utilities.kt +++ b/app/src/main/java/com/schoolapp/cleverclass/Utilities.kt @@ -1,5 +1,15 @@ package com.schoolapp.cleverclass +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + + +const val NO_INTERNET_CONNECTION_CODE = -1 + fun formatFloat(number: Float): String { var formattedString = number.toString() @@ -9,4 +19,14 @@ fun formatFloat(number: Float): String { formattedString = "0.0" return formattedString +} + +@Composable +fun LoadingScreen(){ + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ){ + CircularProgressIndicator() + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_visibility_24.xml b/app/src/main/res/drawable/baseline_visibility_24.xml new file mode 100644 index 0000000..b923c39 --- /dev/null +++ b/app/src/main/res/drawable/baseline_visibility_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_visibility_off_24.xml b/app/src/main/res/drawable/baseline_visibility_off_24.xml new file mode 100644 index 0000000..00c8a20 --- /dev/null +++ b/app/src/main/res/drawable/baseline_visibility_off_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6815568..23f475c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,4 +9,5 @@ DSBActivity MebisActivity DSBDayViewActivity + DSBLoginActivity \ No newline at end of file