feat: Improve JSON import error handling and logging

This commit introduces several improvements to the JSON import functionality:

- Enhanced error handling for both manual and automatic JSON imports in  and .
  - Implemented graceful fallback to unencrypted parsing if decryption fails.
  - Added specific error handling for  and generic  during JSON parsing.
  - Provided user-friendly Toast messages for import failures.
- Improved error logging in  for  when encrypted data is too short.
- Added missing string resources for import error messages and their German translations.
- Ensured correct placement of  resource to avoid duplicates and created missing .
- Added debug logging to show raw JSON strings before parsing for better diagnostics.

These changes make the import process more robust and provide clearer feedback to the user in case of corrupted or malformed import files.
This commit is contained in:
2025-11-01 15:06:52 +01:00
parent c0d36e82f8
commit 34f4caeff2
6 changed files with 134 additions and 21 deletions

View File

@@ -100,6 +100,7 @@ import de.lxtools.noteshop.ui.theme.NoteshopTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import android.content.Intent
import kotlinx.serialization.SerializationException
import android.content.Context
import android.content.Intent.ACTION_SEND
import android.net.Uri
@@ -641,7 +642,15 @@ fun AppShell(
}
if (jsonString.isNotBlank()) {
notesViewModel.importNotesFromJson(jsonString)
Log.d("AppShell", "Notes JSON string: $jsonString")
try {
} catch (e: kotlinx.serialization.SerializationException) {
Log.e("AppShell", "Error parsing notes JSON: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_invalid_format, e.message), android.widget.Toast.LENGTH_LONG).show()
} catch (e: Exception) {
Log.e("AppShell", "Error importing notes: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_generic, e.message), android.widget.Toast.LENGTH_LONG).show()
}
}
}
}
@@ -664,7 +673,16 @@ fun AppShell(
}
if (jsonString.isNotBlank()) {
shoppingListsViewModel.importShoppingListsFromJson(jsonString)
Log.d("AppShell", "Shopping Lists JSON string: $jsonString")
try {
shoppingListsViewModel.importShoppingListsFromJson(jsonString)
} catch (e: kotlinx.serialization.SerializationException) {
Log.e("AppShell", "Error parsing shopping lists JSON: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_invalid_format, e.message), android.widget.Toast.LENGTH_LONG).show()
} catch (e: Exception) {
Log.e("AppShell", "Error importing shopping lists: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_generic, e.message), android.widget.Toast.LENGTH_LONG).show()
}
}
}
}
@@ -687,7 +705,16 @@ fun AppShell(
}
if (jsonString.isNotBlank()) {
recipesViewModel.importRecipesFromJson(jsonString)
Log.d("AppShell", "Recipes JSON string: $jsonString")
try {
recipesViewModel.importRecipesFromJson(jsonString)
} catch (e: kotlinx.serialization.SerializationException) {
Log.e("AppShell", "Error parsing recipes JSON: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_invalid_format, e.message), android.widget.Toast.LENGTH_LONG).show()
} catch (e: Exception) {
Log.e("AppShell", "Error importing recipes: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_generic, e.message), android.widget.Toast.LENGTH_LONG).show()
}
}
}
}

View File

@@ -62,6 +62,9 @@ class FileEncryptor {
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec)
return cipher.doFinal(encryptedData)
} catch (e: IllegalArgumentException) {
Log.e("FileEncryptor", "Error decrypting data: Invalid encrypted data format (too short for IV).", e)
throw e
} catch (e: Exception) {
Log.e("FileEncryptor", "Error decrypting data", e)
throw e

View File

@@ -22,6 +22,8 @@ import androidx.compose.ui.res.stringResource
import de.lxtools.noteshop.security.FileEncryptor
import androidx.compose.ui.unit.dp
import de.lxtools.noteshop.R
import android.widget.Toast
import kotlinx.serialization.SerializationException
import de.lxtools.noteshop.ui.notes.NotesViewModel
import de.lxtools.noteshop.ui.shopping.ShoppingListsViewModel
import de.lxtools.noteshop.ui.recipes.RecipesViewModel
@@ -87,12 +89,36 @@ fun JsonImportExportDialog(
uri?.let {
scope.launch {
context.contentResolver.openInputStream(it)?.use { inputStream ->
secretKey?.let { key ->
val encryptedContent = inputStream.readBytes()
val decryptedBytes = fileEncryptor.decrypt(encryptedContent, key)
val jsonString = decryptedBytes.toString(Charsets.UTF_8)
notesViewModel.importNotesFromJson(jsonString)
android.widget.Toast.makeText(context, R.string.json_import_successful, android.widget.Toast.LENGTH_SHORT).show()
val fileContent = inputStream.readBytes()
var jsonString: String? = null
if (secretKey != null) {
try {
val decryptedBytes = fileEncryptor.decrypt(fileContent, secretKey!!)
jsonString = decryptedBytes.toString(Charsets.UTF_8)
} catch (e: javax.crypto.AEADBadTagException) {
android.util.Log.w("JsonImportExportDialog", "Decryption failed, trying as unencrypted: ${e.message}")
// Fallback to unencrypted if decryption fails
jsonString = fileContent.toString(Charsets.UTF_8)
}
} else {
// If no secret key, assume unencrypted
jsonString = fileContent.toString(Charsets.UTF_8)
}
if (jsonString != null) {
try {
notesViewModel.importNotesFromJson(jsonString)
android.widget.Toast.makeText(context, R.string.json_import_successful, android.widget.Toast.LENGTH_SHORT).show()
} catch (e: kotlinx.serialization.SerializationException) {
android.util.Log.e("JsonImportExportDialog", "Error parsing notes JSON: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_invalid_format, e.message), android.widget.Toast.LENGTH_LONG).show()
} catch (e: Exception) {
android.util.Log.e("JsonImportExportDialog", "Error importing notes: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_generic, e.message), android.widget.Toast.LENGTH_LONG).show()
}
} else {
android.widget.Toast.makeText(context, R.string.json_import_failed_no_data, android.widget.Toast.LENGTH_SHORT).show()
}
}
}
@@ -147,12 +173,36 @@ fun JsonImportExportDialog(
uri?.let {
scope.launch {
context.contentResolver.openInputStream(it)?.use { inputStream ->
secretKey?.let { key ->
val encryptedContent = inputStream.readBytes()
val decryptedBytes = fileEncryptor.decrypt(encryptedContent, key)
val jsonString = decryptedBytes.toString(Charsets.UTF_8)
shoppingListsViewModel.importShoppingListsFromJson(jsonString)
android.widget.Toast.makeText(context, R.string.json_import_successful, android.widget.Toast.LENGTH_SHORT).show()
val fileContent = inputStream.readBytes()
var jsonString: String? = null
if (secretKey != null) {
try {
val decryptedBytes = fileEncryptor.decrypt(fileContent, secretKey!!)
jsonString = decryptedBytes.toString(Charsets.UTF_8)
} catch (e: javax.crypto.AEADBadTagException) {
android.util.Log.w("JsonImportExportDialog", "Decryption failed, trying as unencrypted: ${e.message}")
// Fallback to unencrypted if decryption fails
jsonString = fileContent.toString(Charsets.UTF_8)
}
} else {
// If no secret key, assume unencrypted
jsonString = fileContent.toString(Charsets.UTF_8)
}
if (jsonString != null) {
try {
shoppingListsViewModel.importShoppingListsFromJson(jsonString)
android.widget.Toast.makeText(context, R.string.json_import_successful, android.widget.Toast.LENGTH_SHORT).show()
} catch (e: kotlinx.serialization.SerializationException) {
android.util.Log.e("JsonImportExportDialog", "Error parsing shopping lists JSON: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_invalid_format, e.message), android.widget.Toast.LENGTH_LONG).show()
} catch (e: Exception) {
android.util.Log.e("JsonImportExportDialog", "Error importing shopping lists: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_generic, e.message), android.widget.Toast.LENGTH_LONG).show()
}
} else {
android.widget.Toast.makeText(context, R.string.json_import_failed_no_data, android.widget.Toast.LENGTH_SHORT).show()
}
}
}
@@ -187,12 +237,36 @@ fun JsonImportExportDialog(
uri?.let {
scope.launch {
context.contentResolver.openInputStream(it)?.use { inputStream ->
secretKey?.let { key ->
val encryptedContent = inputStream.readBytes()
val decryptedBytes = fileEncryptor.decrypt(encryptedContent, key)
val jsonString = decryptedBytes.toString(Charsets.UTF_8)
recipesViewModel.importRecipesFromJson(jsonString)
android.widget.Toast.makeText(context, R.string.json_import_successful, android.widget.Toast.LENGTH_SHORT).show()
val fileContent = inputStream.readBytes()
var jsonString: String? = null
if (secretKey != null) {
try {
val decryptedBytes = fileEncryptor.decrypt(fileContent, secretKey!!)
jsonString = decryptedBytes.toString(Charsets.UTF_8)
} catch (e: javax.crypto.AEADBadTagException) {
android.util.Log.w("JsonImportExportDialog", "Decryption failed, trying as unencrypted: ${e.message}")
// Fallback to unencrypted if decryption fails
jsonString = fileContent.toString(Charsets.UTF_8)
}
} else {
// If no secret key, assume unencrypted
jsonString = fileContent.toString(Charsets.UTF_8)
}
if (jsonString != null) {
try {
recipesViewModel.importRecipesFromJson(jsonString)
android.widget.Toast.makeText(context, R.string.json_import_successful, android.widget.Toast.LENGTH_SHORT).show()
} catch (e: kotlinx.serialization.SerializationException) {
android.util.Log.e("JsonImportExportDialog", "Error parsing recipes JSON: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_invalid_format, e.message), android.widget.Toast.LENGTH_LONG).show()
} catch (e: Exception) {
android.util.Log.e("JsonImportExportDialog", "Error importing recipes: ${e.message}")
android.widget.Toast.makeText(context, context.getString(R.string.json_import_failed_generic, e.message), android.widget.Toast.LENGTH_LONG).show()
}
} else {
android.widget.Toast.makeText(context, R.string.json_import_failed_no_data, android.widget.Toast.LENGTH_SHORT).show()
}
}
}

View File

@@ -0,0 +1,3 @@
<resources>
<string name="encryption_salt" translatable="false">/y2B/g/y2B/g/y2B/g==</string>
</resources>

View File

@@ -315,4 +315,7 @@
<string name="tour_page_3_title">Sicher &amp; Privat</string>
<string name="tour_page_3_text">Deine Daten gehören dir. Verschlüssele deine Daten für maximale Privatsphäre.</string>
<string name="show_guided_tour">Geführte Tour anzeigen</string>
<string name="json_import_failed_invalid_format">JSON-Import fehlgeschlagen: Ungültiges Format. %1$s</string>
<string name="json_import_failed_generic">JSON-Import fehlgeschlagen: %1$s</string>
<string name="json_import_failed_no_data">JSON-Import fehlgeschlagen: Keine Daten zum Importieren.</string>
</resources>

View File

@@ -315,4 +315,7 @@
<string name="tour_page_3_title">Secure &amp; Private</string>
<string name="tour_page_3_text">Your data is yours. Encrypt your data for maximum privacy.</string>
<string name="show_guided_tour">Show Guided Tour</string>
<string name="json_import_failed_invalid_format">JSON import failed: Invalid format. %1$s</string>
<string name="json_import_failed_generic">JSON import failed: %1$s</string>
<string name="json_import_failed_no_data">JSON import failed: No data to import.</string>
</resources>