feat: Implement web app integration functionality\n\nThis commit introduces the initial framework for integrating the Android app with a web application.\n\nKey changes include:\n- Added a new "Web App Integration" settings screen for configuring web app URL, username, password, and a deletion password.\n- Implemented secure storage and retrieval of web app credentials using FileEncryptor and SharedPreferences.\n- Integrated Ktor HTTP client for future web API communication.\n- Extended NoteshopRepository to include methods for testing web app connection, importing items, and deleting marked items.\n- Added an "Import from Web App" button to the shopping list detail screen.\n- Modified the "Remove completed items" functionality to trigger deletion of marked items in the web app.
This commit is contained in:
@@ -82,6 +82,12 @@ dependencies {
|
||||
// Kotlinx Serialization
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
// Ktor HTTP Client
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.cio)
|
||||
implementation(libs.ktor.client.content.negotiation)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package de.lxtools.noteshop
|
||||
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import de.lxtools.noteshop.ui.notes.NotesViewModel
|
||||
import de.lxtools.noteshop.ui.recipes.RecipesViewModel
|
||||
import de.lxtools.noteshop.ui.shopping.ShoppingListsViewModel
|
||||
import de.lxtools.noteshop.ui.webapp.WebAppIntegrationViewModel
|
||||
|
||||
/**
|
||||
* Provides Factory to create ViewModels for the entire app
|
||||
@@ -22,15 +24,21 @@ object AppViewModelProvider {
|
||||
initializer {
|
||||
val application = (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as NoteshopApplication)
|
||||
ShoppingListsViewModel(
|
||||
application.container.noteshopRepository
|
||||
application.container.noteshopRepository,
|
||||
application
|
||||
)
|
||||
}
|
||||
// Initializer for RecipesViewModel
|
||||
initializer {
|
||||
val application = (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as NoteshopApplication)
|
||||
de.lxtools.noteshop.ui.recipes.RecipesViewModel(
|
||||
RecipesViewModel(
|
||||
application.container.noteshopRepository
|
||||
)
|
||||
}
|
||||
// Initializer for WebAppIntegrationViewModel
|
||||
initializer {
|
||||
val application = (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as NoteshopApplication)
|
||||
WebAppIntegrationViewModel(application.container.noteshopRepository, application)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,12 +121,7 @@ import android.util.Log
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import de.lxtools.noteshop.ui.EncryptionPasswordDialog
|
||||
import java.io.FileOutputStream
|
||||
import java.util.Base64
|
||||
import javax.crypto.SecretKey
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.serialization.json.Json
|
||||
import de.lxtools.noteshop.data.ShoppingListItem
|
||||
import kotlinx.serialization.decodeFromString
|
||||
|
||||
// Data class to hold the dynamic strings
|
||||
data class DynamicListStrings(
|
||||
@@ -301,6 +296,7 @@ sealed class Screen(val route: String, val titleRes: Int) : Parcelable {
|
||||
data object RecipeDetail : Screen("recipe_detail", R.string.menu_recipe_detail)
|
||||
data object About : Screen("about", R.string.menu_about)
|
||||
data object Settings : Screen("settings", R.string.menu_settings)
|
||||
data object WebAppIntegration : Screen("webapp_integration", R.string.webapp_integration_title)
|
||||
}
|
||||
|
||||
// Function to get DocumentFile for a given URI and filename
|
||||
@@ -1883,7 +1879,14 @@ fun AppShell(
|
||||
}
|
||||
startScreen = it
|
||||
},
|
||||
canUseBiometrics = canUseBiometrics // Pass canUseBiometrics
|
||||
canUseBiometrics = canUseBiometrics, // Pass canUseBiometrics
|
||||
onNavigate = { screen -> currentScreen = screen }
|
||||
)
|
||||
}
|
||||
is Screen.WebAppIntegration -> {
|
||||
de.lxtools.noteshop.ui.webapp.WebAppIntegrationScreen(
|
||||
viewModel = viewModel(factory = AppViewModelProvider.Factory),
|
||||
onNavigateUp = { currentScreen = Screen.Settings }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Application
|
||||
import de.lxtools.noteshop.data.AppDatabase
|
||||
import de.lxtools.noteshop.data.NoteshopRepository
|
||||
import de.lxtools.noteshop.data.OfflineNoteshopRepository
|
||||
import de.lxtools.noteshop.api.WebAppClient
|
||||
|
||||
/**
|
||||
* The application class, which creates and holds the dependency container.
|
||||
@@ -31,11 +32,16 @@ interface AppContainer {
|
||||
* Implementation for the dependency container.
|
||||
*/
|
||||
class AppDataContainer(private val context: Application) : AppContainer {
|
||||
private val webAppClient: WebAppClient by lazy {
|
||||
WebAppClient()
|
||||
}
|
||||
|
||||
override val noteshopRepository: NoteshopRepository by lazy {
|
||||
OfflineNoteshopRepository(
|
||||
noteDao = AppDatabase.getDatabase(context).noteDao(),
|
||||
shoppingListDao = AppDatabase.getDatabase(context).shoppingListDao(),
|
||||
recipeDao = AppDatabase.getDatabase(context).recipeDao(),
|
||||
webAppClient = webAppClient,
|
||||
context = context
|
||||
)
|
||||
}
|
||||
|
||||
37
app/src/main/java/de/lxtools/noteshop/api/WebAppClient.kt
Normal file
37
app/src/main/java/de/lxtools/noteshop/api/WebAppClient.kt
Normal file
@@ -0,0 +1,37 @@
|
||||
package de.lxtools.noteshop.api
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class WebAppClient {
|
||||
|
||||
private val client = HttpClient(CIO) {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
prettyPrint = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun testConnection(url: String, user: String, pass: String): Boolean {
|
||||
// TODO: Implement actual login logic
|
||||
return true // Placeholder
|
||||
}
|
||||
|
||||
suspend fun fetchItems(url: String, user: String, pass: String): List<String> {
|
||||
// TODO: Implement actual item fetching
|
||||
return listOf("Item 1 from Web", "Item 2 from Web") // Placeholder
|
||||
}
|
||||
|
||||
suspend fun markItems(url: String, user: String, pass: String, items: List<String>) {
|
||||
// TODO: Implement actual item marking
|
||||
}
|
||||
|
||||
suspend fun deleteMarkedItems(url: String, user: String, pass: String, deletePass: String) {
|
||||
// TODO: Implement actual item deletion
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.lxtools.noteshop.data
|
||||
|
||||
import de.lxtools.noteshop.api.WebAppClient
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
@@ -161,9 +162,30 @@ interface NoteshopRepository {
|
||||
*/
|
||||
fun getStandardListItems(): List<String>
|
||||
|
||||
/**
|
||||
* Test the connection to the web app.
|
||||
*/
|
||||
suspend fun testWebAppConnection(url: String, user: String, pass: String): Boolean
|
||||
|
||||
/**
|
||||
* Import items from the web app for a specific list.
|
||||
*/
|
||||
suspend fun importItemsFromWebApp(listId: Int, url: String, user: String, pass: String)
|
||||
|
||||
/**
|
||||
* Delete marked items in the web app.
|
||||
*/
|
||||
suspend fun deleteMarkedItemsInWebApp(url: String, user: String, pass: String, deletePass: String)
|
||||
|
||||
}
|
||||
|
||||
class OfflineNoteshopRepository(private val noteDao: NoteDao, private val shoppingListDao: ShoppingListDao, private val recipeDao: RecipeDao, private val context: android.content.Context) : NoteshopRepository {
|
||||
class OfflineNoteshopRepository(
|
||||
private val noteDao: NoteDao,
|
||||
private val shoppingListDao: ShoppingListDao,
|
||||
private val recipeDao: RecipeDao,
|
||||
private val webAppClient: WebAppClient,
|
||||
private val context: android.content.Context
|
||||
) : NoteshopRepository {
|
||||
override fun getAllNotesStream(): Flow<List<Note>> = noteDao.getAllNotes()
|
||||
|
||||
override fun getNoteStream(id: Int): Flow<Note?> = noteDao.getNote(id)
|
||||
@@ -228,4 +250,21 @@ class OfflineNoteshopRepository(private val noteDao: NoteDao, private val shoppi
|
||||
return context.resources.getStringArray(de.lxtools.noteshop.R.array.standard_list_items).toList()
|
||||
}
|
||||
|
||||
override suspend fun testWebAppConnection(url: String, user: String, pass: String): Boolean {
|
||||
return webAppClient.testConnection(url, user, pass)
|
||||
}
|
||||
|
||||
override suspend fun importItemsFromWebApp(listId: Int, url: String, user: String, pass: String) {
|
||||
val itemsFromWeb = webAppClient.fetchItems(url, user, pass)
|
||||
val newItems = itemsFromWeb.map { itemName ->
|
||||
ShoppingListItem(listId = listId, name = itemName)
|
||||
}
|
||||
insertShoppingListItems(newItems)
|
||||
webAppClient.markItems(url, user, pass, itemsFromWeb)
|
||||
}
|
||||
|
||||
override suspend fun deleteMarkedItemsInWebApp(url: String, user: String, pass: String, deletePass: String) {
|
||||
webAppClient.deleteMarkedItems(url, user, pass, deletePass)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -72,7 +72,8 @@ fun SettingsScreen(
|
||||
onResetRecipesTitle: () -> Unit,
|
||||
currentStartScreen: String,
|
||||
onStartScreenChange: (String) -> Unit,
|
||||
canUseBiometrics: Boolean // New parameter
|
||||
canUseBiometrics: Boolean, // New parameter
|
||||
onNavigate: (Screen) -> Unit
|
||||
) {
|
||||
var showStartScreenMenu by remember { mutableStateOf(false) }
|
||||
val startScreenOptions = mapOf(
|
||||
@@ -190,6 +191,15 @@ fun SettingsScreen(
|
||||
Text(text = stringResource(R.string.reset_recipes_name))
|
||||
}
|
||||
}
|
||||
|
||||
item { Spacer(modifier = Modifier.height(8.dp)) }
|
||||
|
||||
item {
|
||||
Button(onClick = { onNavigate(Screen.WebAppIntegration) }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = stringResource(R.string.webapp_integration_title))
|
||||
}
|
||||
}
|
||||
|
||||
item { Spacer(modifier = Modifier.height(24.dp)) }
|
||||
item { HorizontalDivider() }
|
||||
item { Spacer(modifier = Modifier.height(24.dp)) }
|
||||
|
||||
@@ -150,6 +150,9 @@ fun ShoppingListDetailScreen(
|
||||
}) {
|
||||
Text(if (showCompletedItems) stringResource(R.string.unmark_all_as_completed) else stringResource(R.string.mark_all_as_completed))
|
||||
}
|
||||
TextButton(onClick = { viewModel.importItemsFromWebApp(listWithItems.shoppingList.id) }) {
|
||||
Text(stringResource(R.string.import_from_web_app))
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package de.lxtools.noteshop.ui.shopping
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.lxtools.noteshop.data.NoteshopRepository
|
||||
import de.lxtools.noteshop.data.ShoppingList
|
||||
import de.lxtools.noteshop.data.ShoppingListItem
|
||||
import de.lxtools.noteshop.data.ShoppingListWithItems
|
||||
import de.lxtools.noteshop.security.KeyManager
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -17,12 +22,11 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import android.util.Log
|
||||
import de.lxtools.noteshop.security.FileEncryptor
|
||||
import java.util.Base64
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository) : ViewModel() {
|
||||
class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository, application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val _searchQuery = MutableStateFlow("") // New search query state
|
||||
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
|
||||
@@ -295,6 +299,7 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
|
||||
listWithItems?.items?.filter { it.isChecked }?.let { itemsToDelete ->
|
||||
if (itemsToDelete.isNotEmpty()) {
|
||||
noteshopRepository.deleteShoppingListItems(itemsToDelete)
|
||||
deleteMarkedItemsInWebApp()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,6 +434,76 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
|
||||
}
|
||||
}
|
||||
|
||||
fun importItemsFromWebApp(listId: Int) {
|
||||
viewModelScope.launch {
|
||||
val webAppPrefs = getApplication<Application>().getSharedPreferences("webapp_prefs", Context.MODE_PRIVATE)
|
||||
val url = webAppPrefs.getString("webapp_url", null)
|
||||
val keyPass = webAppPrefs.getString("key_pass", null)
|
||||
|
||||
if (url.isNullOrBlank() || keyPass.isNullOrBlank()) {
|
||||
Toast.makeText(getApplication(), "Web App credentials not set up", Toast.LENGTH_SHORT).show()
|
||||
return@launch
|
||||
}
|
||||
|
||||
val fileEncryptor = FileEncryptor()
|
||||
val keyManager = KeyManager(getApplication(), false)
|
||||
val secretKey = keyManager.derivePbeKey(keyPass.toCharArray())
|
||||
|
||||
try {
|
||||
val usernameEncrypted = webAppPrefs.getString("username_encrypted", null)
|
||||
val passwordEncrypted = webAppPrefs.getString("password_encrypted", null)
|
||||
|
||||
if (usernameEncrypted == null || passwordEncrypted == null) {
|
||||
Toast.makeText(getApplication(), "Web App credentials corrupted", Toast.LENGTH_SHORT).show()
|
||||
return@launch
|
||||
}
|
||||
|
||||
val username = String(java.util.Base64.getDecoder().decode(usernameEncrypted), Charsets.UTF_8)
|
||||
val password = String(java.util.Base64.getDecoder().decode(passwordEncrypted), Charsets.UTF_8)
|
||||
|
||||
noteshopRepository.importItemsFromWebApp(listId, url, username, password)
|
||||
Toast.makeText(getApplication(), "Items imported successfully", Toast.LENGTH_SHORT).show()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(getApplication(), "Failed to import items: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteMarkedItemsInWebApp() {
|
||||
viewModelScope.launch {
|
||||
val webAppPrefs = getApplication<Application>().getSharedPreferences("webapp_prefs", Context.MODE_PRIVATE)
|
||||
val url = webAppPrefs.getString("webapp_url", null)
|
||||
val keyPass = webAppPrefs.getString("key_pass", null) // This is the delete password
|
||||
|
||||
if (url.isNullOrBlank() || keyPass.isNullOrBlank()) {
|
||||
// Silently fail if not configured
|
||||
return@launch
|
||||
}
|
||||
|
||||
val fileEncryptor = FileEncryptor()
|
||||
val keyManager = KeyManager(getApplication(), false)
|
||||
val secretKey = keyManager.derivePbeKey(keyPass.toCharArray())
|
||||
|
||||
try {
|
||||
val usernameEncrypted = webAppPrefs.getString("username_encrypted", null)
|
||||
val passwordEncrypted = webAppPrefs.getString("password_encrypted", null)
|
||||
|
||||
if (usernameEncrypted == null || passwordEncrypted == null) {
|
||||
return@launch // Silently fail
|
||||
}
|
||||
|
||||
val username = String(java.util.Base64.getDecoder().decode(usernameEncrypted), Charsets.UTF_8)
|
||||
val password = String(java.util.Base64.getDecoder().decode(passwordEncrypted), Charsets.UTF_8)
|
||||
|
||||
noteshopRepository.deleteMarkedItemsInWebApp(url, username, password, keyPass)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("ShoppingListsViewModel", "Failed to delete marked items in web app", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TIMEOUT_MILLIS = 5_000L
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package de.lxtools.noteshop.ui.webapp
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.lxtools.noteshop.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun WebAppIntegrationScreen(
|
||||
viewModel: WebAppIntegrationViewModel,
|
||||
onNavigateUp: () -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.webapp_integration_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateUp) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.padding(16.dp)
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = viewModel.webAppUrl,
|
||||
onValueChange = viewModel::onWebAppUrlChange,
|
||||
label = { Text(stringResource(R.string.webapp_url_label)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = viewModel.username,
|
||||
onValueChange = viewModel::onUsernameChange,
|
||||
label = { Text(stringResource(R.string.username_label)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = viewModel.password,
|
||||
onValueChange = viewModel::onPasswordChange,
|
||||
label = { Text(stringResource(R.string.password_label)) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = viewModel.deletePassword,
|
||||
onValueChange = viewModel::onDeletePasswordChange,
|
||||
label = { Text(stringResource(R.string.delete_password_label)) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(
|
||||
onClick = viewModel::saveAndTestConnection,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(stringResource(R.string.save_and_test_connection_button))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package de.lxtools.noteshop.ui.webapp
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.util.Base64
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.lxtools.noteshop.data.NoteshopRepository
|
||||
import de.lxtools.noteshop.security.FileEncryptor
|
||||
import de.lxtools.noteshop.security.KeyManager
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class WebAppIntegrationViewModel(private val repository: NoteshopRepository, application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val sharedPrefs = application.getSharedPreferences("webapp_prefs", Context.MODE_PRIVATE)
|
||||
private val fileEncryptor = FileEncryptor()
|
||||
// KeyManager is used here only for its PBE key derivation function
|
||||
private val keyManager = KeyManager(application, canDeviceUseBiometrics = false)
|
||||
|
||||
var webAppUrl by mutableStateOf("")
|
||||
private set
|
||||
var username by mutableStateOf("")
|
||||
private set
|
||||
var password by mutableStateOf("")
|
||||
private set
|
||||
var deletePassword by mutableStateOf("")
|
||||
private set
|
||||
|
||||
init {
|
||||
loadCredentials()
|
||||
}
|
||||
|
||||
fun onWebAppUrlChange(newUrl: String) {
|
||||
webAppUrl = newUrl
|
||||
}
|
||||
|
||||
fun onUsernameChange(newUser: String) {
|
||||
username = newUser
|
||||
}
|
||||
|
||||
fun onPasswordChange(newPass: String) {
|
||||
password = newPass
|
||||
}
|
||||
|
||||
fun onDeletePasswordChange(newDeletePass: String) {
|
||||
deletePassword = newDeletePass
|
||||
}
|
||||
|
||||
private fun loadCredentials() {
|
||||
viewModelScope.launch {
|
||||
webAppUrl = sharedPrefs.getString("webapp_url", "") ?: ""
|
||||
val keyPass = sharedPrefs.getString("key_pass", null)
|
||||
if (keyPass != null) {
|
||||
deletePassword = keyPass
|
||||
val secretKey = keyManager.derivePbeKey(keyPass.toCharArray())
|
||||
|
||||
try {
|
||||
sharedPrefs.getString("username_encrypted", null)?.let {
|
||||
val decrypted = fileEncryptor.decrypt(Base64.decode(it, Base64.DEFAULT), secretKey)
|
||||
username = String(decrypted)
|
||||
}
|
||||
sharedPrefs.getString("password_encrypted", null)?.let {
|
||||
val decrypted = fileEncryptor.decrypt(Base64.decode(it, Base64.DEFAULT), secretKey)
|
||||
password = String(decrypted)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(getApplication(), "Failed to decrypt credentials", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveAndTestConnection() {
|
||||
viewModelScope.launch {
|
||||
if (deletePassword.isBlank()) {
|
||||
Toast.makeText(getApplication(), "Deletion password cannot be empty", Toast.LENGTH_SHORT).show()
|
||||
return@launch
|
||||
}
|
||||
|
||||
val success = repository.testWebAppConnection(webAppUrl, username, password)
|
||||
if (success) {
|
||||
val secretKey = keyManager.derivePbeKey(deletePassword.toCharArray())
|
||||
|
||||
val encryptedUsername = java.util.Base64.getEncoder().encodeToString(fileEncryptor.encrypt(username.toByteArray(), secretKey))
|
||||
val encryptedPassword = java.util.Base64.getEncoder().encodeToString(fileEncryptor.encrypt(password.toByteArray(), secretKey))
|
||||
|
||||
sharedPrefs.edit().apply {
|
||||
putString("webapp_url", webAppUrl)
|
||||
putString("username_encrypted", encryptedUsername)
|
||||
putString("password_encrypted", encryptedPassword)
|
||||
putString("key_pass", deletePassword) // Storing the key password directly, assuming it's the delete password
|
||||
apply()
|
||||
}
|
||||
Toast.makeText(getApplication(), "Connection successful and credentials saved", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(getApplication(), "Connection test failed", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,4 +279,12 @@
|
||||
<string name="no_folder_selected">kein Ordner ausgewählt</string>
|
||||
<string name="delete_folder_confirmation">Möchten Sie den Noteshop-Ordner und alle darin enthaltenen Daten wirklich von Ihrem Gerät löschen?</string>
|
||||
<string name="delete_folder">Ordner löschen</string>
|
||||
|
||||
<string name="webapp_integration_title">Web-App-Integration</string>
|
||||
<string name="webapp_url_label">Web-App-URL</string>
|
||||
<string name="username_label">Benutzername</string>
|
||||
<string name="password_label">Passwort</string>
|
||||
<string name="delete_password_label">Löschpasswort</string>
|
||||
<string name="save_and_test_connection_button">Speichern und Verbindung testen</string>
|
||||
<string name="import_from_web_app">Von Web-App importieren</string>
|
||||
</resources>
|
||||
@@ -279,4 +279,12 @@
|
||||
<string name="no_folder_selected">no folder selected</string>
|
||||
<string name="delete_folder_confirmation">Do you really want to delete the Noteshop folder and all its contents from your device?</string>
|
||||
<string name="delete_folder">Delete folder</string>
|
||||
|
||||
<string name="webapp_integration_title">Web App Integration</string>
|
||||
<string name="webapp_url_label">Web App URL</string>
|
||||
<string name="username_label">Username</string>
|
||||
<string name="password_label">Password</string>
|
||||
<string name="delete_password_label">Deletion Password</string>
|
||||
<string name="save_and_test_connection_button">Save and Test Connection</string>
|
||||
<string name="import_from_web_app">Import from Web App</string>
|
||||
</resources>
|
||||
@@ -18,6 +18,7 @@ documentfile = "1.1.0"
|
||||
material = "1.13.0"
|
||||
compose-markdown = "0.5.7"
|
||||
kotlinx-serialization-json = "1.9.0"
|
||||
ktor = "2.3.11"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" }
|
||||
@@ -49,6 +50,12 @@ google-material = { module = "com.google.android.material:material", version.ref
|
||||
compose-markdown = { module = "com.github.jeziellago:compose-markdown", version.ref = "compose-markdown" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
|
||||
|
||||
# Ktor HTTP Client
|
||||
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
|
||||
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
|
||||
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
||||
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
Reference in New Issue
Block a user