Twelve: Ensure server is proper

diff --git a/app/src/main/java/org/lineageos/twelve/datasources/SubsonicDataSource.kt b/app/src/main/java/org/lineageos/twelve/datasources/SubsonicDataSource.kt
index 50f62b0..b406700 100644
--- a/app/src/main/java/org/lineageos/twelve/datasources/SubsonicDataSource.kt
+++ b/app/src/main/java/org/lineageos/twelve/datasources/SubsonicDataSource.kt
@@ -11,6 +11,7 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asFlow
 import kotlinx.coroutines.flow.mapLatest
+import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
 import org.lineageos.twelve.R
 import org.lineageos.twelve.datasources.subsonic.SubsonicClient
 import org.lineageos.twelve.datasources.subsonic.models.AlbumID3
@@ -325,6 +326,16 @@
             R.string.provider_argument_server,
             required = true,
             hidden = false,
+            validate = {
+                when (it.toHttpUrlOrNull()) {
+                    null -> ProviderArgument.ValidationError(
+                        "Invalid URL",
+                        R.string.provider_argument_validation_error_malformed_http_uri,
+                    )
+
+                    else -> null
+                }
+            }
         )
 
         val ARG_USERNAME = ProviderArgument(
diff --git a/app/src/main/java/org/lineageos/twelve/fragments/ManageProviderFragment.kt b/app/src/main/java/org/lineageos/twelve/fragments/ManageProviderFragment.kt
index bdedfe0..1b59fef 100644
--- a/app/src/main/java/org/lineageos/twelve/fragments/ManageProviderFragment.kt
+++ b/app/src/main/java/org/lineageos/twelve/fragments/ManageProviderFragment.kt
@@ -33,7 +33,7 @@
 import org.lineageos.twelve.ext.getViewProperty
 import org.lineageos.twelve.ext.selectItem
 import org.lineageos.twelve.models.ProviderArgument
-import org.lineageos.twelve.models.ProviderArgument.Companion.getArgument
+import org.lineageos.twelve.models.ProviderArgument.Companion.validateArgument
 import org.lineageos.twelve.models.ProviderType
 import org.lineageos.twelve.models.RequestStatus
 import org.lineageos.twelve.ui.recyclerview.SimpleListAdapter
@@ -169,14 +169,14 @@
                 return@setOnClickListener
             }
 
-            val wrongArguments = providerType.arguments.filter { argument ->
-                val value = providerArguments.getArgument(argument)
-
-                (value ?: argument.defaultValue) == null && argument.required
+            val wrongArguments = providerType.arguments.mapNotNull { argument ->
+                providerArguments.validateArgument(argument)?.let {
+                    argument to it
+                }
             }
 
             if (wrongArguments.isNotEmpty()) {
-                showMissingArgumentsDialog(wrongArguments)
+                showArgumentValidationErrorDialog(wrongArguments)
                 return@setOnClickListener
             }
 
@@ -303,13 +303,20 @@
         super.onDestroyView()
     }
 
-    private fun showMissingArgumentsDialog(wrongArguments: List<ProviderArgument<*>>) {
+    private fun showArgumentValidationErrorDialog(
+        wrongArguments: List<Pair<ProviderArgument<*>, ProviderArgument.ValidationError>>
+    ) {
         MaterialAlertDialogBuilder(requireContext())
+            .setTitle(R.string.argument_validation_error_title)
             .setMessage(
                 getString(
-                    R.string.missing_provider_arguments,
-                    wrongArguments.joinToString {
-                        getString(it.nameStringResId)
+                    R.string.argument_validation_error_message,
+                    wrongArguments.joinToString(separator = "\n") {
+                        getString(
+                            R.string.argument_validation_error_item,
+                            getString(it.first.nameStringResId),
+                            getString(it.second.messageStringResId),
+                        )
                     }
                 )
             )
diff --git a/app/src/main/java/org/lineageos/twelve/models/ProviderArgument.kt b/app/src/main/java/org/lineageos/twelve/models/ProviderArgument.kt
index 1709255..e48b35e 100644
--- a/app/src/main/java/org/lineageos/twelve/models/ProviderArgument.kt
+++ b/app/src/main/java/org/lineageos/twelve/models/ProviderArgument.kt
@@ -7,6 +7,7 @@
 
 import android.os.Bundle
 import androidx.annotation.StringRes
+import org.lineageos.twelve.R
 import kotlin.reflect.KClass
 import kotlin.reflect.cast
 
@@ -20,6 +21,8 @@
  * @param required Whether this argument is required
  * @param hidden Whether the value of this argument should be hidden
  * @param defaultValue The default value of the argument
+ * @param validate A lambda to validate the value of the argument, returning a
+ *   [ProviderArgument.ValidationError] if the value is invalid, null otherwise
  */
 data class ProviderArgument<T : Any>(
     val key: String,
@@ -28,28 +31,76 @@
     val required: Boolean,
     val hidden: Boolean,
     val defaultValue: T? = null,
+    val validate: ((T) -> ValidationError?) = { null },
 ) {
-    fun getValue(value: T?) = value ?: defaultValue
+    /**
+     * Validation error of a [ProviderArgument].
+     *
+     * @param message The error message
+     * @param messageStringResId The localized error message string resource ID
+     */
+    data class ValidationError(
+        val message: String,
+        @StringRes val messageStringResId: Int,
+    )
 
     companion object {
+        private val requiredValidationError = ValidationError(
+            "A value is required",
+            R.string.provider_argument_validation_error_required,
+        )
+
         /**
-         * Get the optional argument from a [Bundle].
+         * Get the argument value from a [Bundle] or the default value if it is not present.
          */
-        fun <T : Any> Bundle.getArgument(
+        private fun <T : Any> Bundle.getArgumentValue(
             providerArguments: ProviderArgument<T>
-        ) = when (providerArguments.type) {
-            String::class -> getString(providerArguments.key)
-            Boolean::class -> getBoolean(providerArguments.key)
-            else -> throw Exception("Unsupported type")
-        }?.let {
-            providerArguments.getValue(providerArguments.type.cast(it))
+        ) = when (containsKey(providerArguments.key)) {
+            true -> when (providerArguments.type) {
+                String::class -> getString(providerArguments.key)
+                Boolean::class -> getBoolean(providerArguments.key)
+                else -> throw Exception("Unsupported type")
+            }?.let {
+                providerArguments.type.cast(it)
+            }
+
+            false -> providerArguments.defaultValue
         }
 
         /**
-         * Get the required argument from a [Bundle].
+         * Get the optional argument from a [Bundle]. This will also validate the value and throw
+         * an exception if the value is invalid.
+         */
+        fun <T : Any> Bundle.getArgument(
+            providerArguments: ProviderArgument<T>
+        ) = getArgumentValue(providerArguments)?.also { argumentValue ->
+            providerArguments.validate(argumentValue)?.let {
+                throw Exception(
+                    "Validation error for argument ${providerArguments.key}: ${it.message}"
+                )
+            }
+        }
+
+        /**
+         * Get the required argument from a [Bundle]. This will also validate the value and throw
+         * an exception if the value is invalid.
          */
         fun <T : Any> Bundle.requireArgument(
             providerArguments: ProviderArgument<T>
         ) = getArgument(providerArguments) ?: throw Exception("Argument not found")
+
+        /**
+         * Validate the argument in this [Bundle] and return a [ValidationError] if it is invalid.
+         * Will also check if the argument is required.
+         */
+        fun <T : Any> Bundle.validateArgument(
+            providerArguments: ProviderArgument<T>
+        ) = getArgumentValue(providerArguments).let { argumentValue ->
+            argumentValue?.let {
+                providerArguments.validate(it)
+            } ?: requiredValidationError.takeIf {
+                providerArguments.required && argumentValue == null
+            }
+        }
     }
 }
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 929ddd6..2b4f3df 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -116,6 +116,10 @@
     <string name="provider_argument_password">Password</string>
     <string name="provider_argument_use_legacy_authentication">Use legacy authentication</string>
 
+    <!-- Provider arguments validation errors -->
+    <string name="provider_argument_validation_error_required">A value is required</string>
+    <string name="provider_argument_validation_error_malformed_http_uri">Must be a valid HTTP or HTTPS URL (e.g. https://google.com)</string>
+
     <!-- Provider selector dialog fragment -->
     <string name="providers">Providers</string>
     <string name="add_provider_action">Add</string>
@@ -130,7 +134,9 @@
     <string name="provider_name_error">Enter a name</string>
     <string name="provider_type">Provider type</string>
     <string name="provider_type_error">Select a provider type</string>
-    <string name="missing_provider_arguments">The following arguments are required: %1$s</string>
+    <string name="argument_validation_error_title">Error</string>
+    <string name="argument_validation_error_message">Error while parsing the following arguments:\n%1$s</string>
+    <string name="argument_validation_error_item">%1$s: %2$s</string>
 
     <!-- Now playing widget -->
     <string name="now_playing_widget_description">Now playing</string>