diff --git a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt b/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt index 1c38e97..9713b99 100644 --- a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt +++ b/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt @@ -72,9 +72,9 @@ class AddSiteActivity : DarkModeSwitchActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_addsite) - setupUi() setupValidation() + lifecycle.addObserver(viewModel) // Populate view model with initial data diff --git a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt index 358dfd1..f5f4dcc 100644 --- a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt +++ b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt @@ -32,11 +32,13 @@ import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection import com.afollestad.nocknock.utilities.livedata.distinct import com.afollestad.nocknock.utilities.providers.IntentProvider import com.afollestad.nocknock.viewcomponents.ext.dimenFloat +import com.afollestad.nocknock.viewcomponents.ext.isVisibleCondition import com.afollestad.nocknock.viewcomponents.ext.onScroll import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData -import com.afollestad.nocknock.viewcomponents.livedata.toViewError import com.afollestad.nocknock.viewcomponents.livedata.toViewText import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility +import com.afollestad.vvalidator.form +import com.afollestad.vvalidator.form.Form import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalLayout import kotlinx.android.synthetic.main.activity_viewsite.headersLayout import kotlinx.android.synthetic.main.activity_viewsite.iconStatus @@ -69,6 +71,7 @@ class ViewSiteActivity : DarkModeSwitchActivity() { } internal val viewModel by viewModel() + private lateinit var validationForm: Form private val intentProvider by inject() private val statusUpdateReceiver by lazy { @@ -82,6 +85,7 @@ class ViewSiteActivity : DarkModeSwitchActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_viewsite) setupUi() + setupValidation() lifecycle.run { addObserver(viewModel) @@ -103,23 +107,17 @@ class ViewSiteActivity : DarkModeSwitchActivity() { // Name inputName.attachLiveData(this, viewModel.name) - viewModel.onNameError() - .toViewError(this, inputName) // Tags inputTags.attachLiveData(this, viewModel.tags) // Url inputUrl.attachLiveData(this, viewModel.url) - viewModel.onUrlError() - .toViewError(this, inputUrl) viewModel.onUrlWarningVisibility() .toViewVisibility(this, textUrlWarning) // Timeout responseTimeoutInput.attachLiveData(this, viewModel.timeout) - viewModel.onTimeoutError() - .toViewError(this, responseTimeoutInput) // Validation mode responseValidationMode.attachLiveData( @@ -128,8 +126,6 @@ class ViewSiteActivity : DarkModeSwitchActivity() { outTransformer = { ValidationMode.fromIndex(it) }, inTransformer = { it.toIndex() } ) - viewModel.onValidationSearchTermError() - .toViewError(this, responseValidationSearchTerm) viewModel.onValidationModeDescription() .toViewText(this, validationModeDescription) @@ -141,15 +137,15 @@ class ViewSiteActivity : DarkModeSwitchActivity() { // Validation script scriptInputLayout.attach( codeData = viewModel.validationScript, - errorData = viewModel.onValidationScriptError(), - visibility = viewModel.onValidationScriptVisibility() + visibility = viewModel.onValidationScriptVisibility(), + form = validationForm ) // Check interval checkIntervalLayout.attach( valueData = viewModel.checkIntervalValue, multiplierData = viewModel.checkIntervalUnit, - errorData = viewModel.onCheckIntervalError() + form = validationForm ) // Retry Policy @@ -162,8 +158,6 @@ class ViewSiteActivity : DarkModeSwitchActivity() { sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it } viewModel.certificateUri.distinct() .observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) }) - viewModel.onCertificateError() - .toViewError(this, sslCertificateInput) // Headers headersLayout.attach(viewModel.headers) @@ -190,7 +184,6 @@ class ViewSiteActivity : DarkModeSwitchActivity() { setOnMenuItemClickListener { when (it.itemId) { - R.id.commit -> viewModel.commit { finish() } R.id.remove -> maybeRemoveSite() R.id.disableChecks -> maybeDisableChecks() } @@ -238,6 +231,35 @@ class ViewSiteActivity : DarkModeSwitchActivity() { } } + private fun setupValidation() { + validationForm = form { + input(inputName, name = "Name") { + isNotEmpty().description(R.string.please_enter_name) + } + input(inputUrl, name = "URL") { + isNotEmpty().description(R.string.please_enter_url) + isUrl().description(R.string.please_enter_valid_url) + } + input(responseTimeoutInput, name = "Timeout", optional = true) { + isNumber().greaterThan(0) + .description(R.string.please_enter_networkTimeout) + } + input(responseValidationSearchTerm, name = "Search term") { + conditional(responseValidationSearchTerm.isVisibleCondition()) { + isNotEmpty().description(R.string.please_enter_search_term) + } + } + input(sslCertificateInput, name = "Certificate Path") { + isUri().hasScheme("file", "content") + .that { it.host != null } + .description(R.string.please_enter_validCertUri) + } + submitWith(toolbar.menu, R.id.commit) { + viewModel.commit { finish() } + } + } + } + override fun onResume() { super.onResume() appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) { diff --git a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModel.kt b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModel.kt index 91c3125..bc910b9 100644 --- a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModel.kt +++ b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModel.kt @@ -40,11 +40,9 @@ import com.afollestad.nocknock.engine.validation.ValidationExecutor import com.afollestad.nocknock.notifications.NockNotificationManager import com.afollestad.nocknock.ui.ScopedViewModel import com.afollestad.nocknock.utilities.ext.formatDate -import com.afollestad.nocknock.utilities.ext.toUri import com.afollestad.nocknock.utilities.livedata.map import com.afollestad.nocknock.utilities.livedata.zip import com.afollestad.nocknock.utilities.providers.StringProvider -import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -81,23 +79,10 @@ class ViewSiteViewModel( internal val disabled = MutableLiveData() internal val lastResult = MutableLiveData() - // Private properties private val isLoading = MutableLiveData() - private val nameError = MutableLiveData() - private val urlError = MutableLiveData() - private val timeoutError = MutableLiveData() - private val validationSearchTermError = MutableLiveData() - private val validationScriptError = MutableLiveData() - private val checkIntervalValueError = MutableLiveData() - private val certificateError = MutableLiveData() - // Expose private properties or calculated properties @CheckResult fun onIsLoading(): LiveData = isLoading - @CheckResult fun onNameError(): LiveData = nameError - - @CheckResult fun onUrlError(): LiveData = urlError - @CheckResult fun onUrlWarningVisibility(): LiveData { return url.map { val parsed = HttpUrl.parse(it) @@ -105,8 +90,6 @@ class ViewSiteViewModel( } } - @CheckResult fun onTimeoutError(): LiveData = timeoutError - @CheckResult fun onValidationModeDescription(): LiveData { return validationMode.map { when (it!!) { @@ -117,22 +100,11 @@ class ViewSiteViewModel( } } - @CheckResult fun onValidationSearchTermError(): LiveData = validationSearchTermError + @CheckResult fun onValidationSearchTermVisibility() = validationMode.map { it == TERM_SEARCH } - @CheckResult fun onValidationSearchTermVisibility() = - validationMode.map { it == TERM_SEARCH } + @CheckResult fun onValidationScriptVisibility() = validationMode.map { it == JAVASCRIPT } - @CheckResult fun onValidationScriptError(): LiveData = validationScriptError - - @CheckResult fun onValidationScriptVisibility() = - validationMode.map { it == JAVASCRIPT } - - @CheckResult fun onCheckIntervalError(): LiveData = checkIntervalValueError - - @CheckResult fun onDisableChecksVisibility(): LiveData = - disabled.map { !it } - - @CheckResult fun onCertificateError(): LiveData = certificateError + @CheckResult fun onDisableChecksVisibility(): LiveData = disabled.map { !it } @CheckResult fun onDoneButtonText(): LiveData = disabled.map { @@ -250,89 +222,7 @@ class ViewSiteViewModel( } private fun getUpdatedDbModel(): Site? { - var errorCount = 0 - - // Validation name - if (name.value.isNullOrEmpty()) { - nameError.value = R.string.please_enter_name - errorCount++ - } else { - nameError.value = null - } - - // Validate URL - when { - url.value.isNullOrEmpty() -> { - urlError.value = R.string.please_enter_url - errorCount++ - } - HttpUrl.parse(url.value!!) == null -> { - urlError.value = R.string.please_enter_valid_url - errorCount++ - } - else -> { - urlError.value = null - } - } - - // Validate timeout val timeout = timeout.value ?: 10_000 - if (timeout < 0) { - timeoutError.value = R.string.please_enter_networkTimeout - errorCount++ - } else { - timeoutError.value = null - } - - // Validate check interval - if (checkIntervalValue.value.isNullOrLessThan(1)) { - checkIntervalValueError.value = R.string.please_enter_check_interval - errorCount++ - } else { - checkIntervalValueError.value = null - } - - // Validate arguments - if (validationMode.value == TERM_SEARCH && - validationSearchTerm.value.isNullOrEmpty() - ) { - errorCount++ - validationSearchTermError.value = R.string.please_enter_search_term - validationScriptError.value = null - } else if (validationMode.value == JAVASCRIPT && - validationScript.value.isNullOrEmpty() - ) { - errorCount++ - validationSearchTermError.value = null - validationScriptError.value = R.string.please_enter_javaScript - } else { - validationSearchTermError.value = null - validationScriptError.value = null - } - - // Validate SSL certificate - val certString = certificateUri.value - if (certString != null) { - val rawCertUri = certString.toUri() - val certUri = if (rawCertUri.scheme == null) { - rawCertUri.buildUpon() - .scheme("file") - .build() - } else { - rawCertUri - } - if (certUri.scheme != "content" && certUri.scheme != "file") { - errorCount++ - certificateError.value = R.string.please_enter_validCertUri - } else { - certificateError.value = null - } - } - - if (errorCount > 0) { - return null - } - val cleanedTags = tags.value?.split(',')?.joinToString { it.trim() } ?: "" val newSettings = site.settings!!.copy( @@ -349,11 +239,15 @@ class ViewSiteViewModel( val retryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) { if (site.retryPolicy != null) { // Have existing policy, update it - site.retryPolicy!!.copy(count = retryPolicyTimes, minutes = retryPolicyMinutes) + site.retryPolicy!!.copy( + count = retryPolicyTimes, + minutes = retryPolicyMinutes + ) } else { // Create new policy RetryPolicy( - count = retryPolicyTimes, minutes = retryPolicyMinutes + count = retryPolicyTimes, + minutes = retryPolicyMinutes ) } } else { diff --git a/app/src/test/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelTest.kt b/app/src/test/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelTest.kt index 39e017b..d7bc8a8 100644 --- a/app/src/test/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelTest.kt +++ b/app/src/test/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelTest.kt @@ -39,7 +39,6 @@ import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.argumentCaptor import com.nhaarman.mockitokotlin2.doAnswer import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -255,247 +254,9 @@ class ViewSiteViewModelTest { .isEqualTo("Two") } - @Test fun commit_nameError() { - val onNameError = viewModel.onNameError() - .test() - val onUrlError = viewModel.onUrlError() - .test() - val onTimeoutError = viewModel.onTimeoutError() - .test() - val onCheckIntervalError = viewModel.onCheckIntervalError() - .test() - val onSearchTermError = viewModel.onValidationSearchTermError() - .test() - val onScriptError = viewModel.onValidationScriptError() - .test() - - fillInModel().apply { - name.value = "" - } - val onDone = mock<() -> Unit>() - viewModel.commit(onDone) - - verify(validationManager, never()) - .scheduleValidation(any(), any(), any(), any()) - onNameError.assertValues(R.string.please_enter_name) - onUrlError.assertNoValues() - onTimeoutError.assertNoValues() - onCheckIntervalError.assertNoValues() - onSearchTermError.assertNoValues() - onScriptError.assertNoValues() - - verify(onDone, never()).invoke() - } - - @Test fun commit_urlEmptyError() { - val onNameError = viewModel.onNameError() - .test() - val onUrlError = viewModel.onUrlError() - .test() - val onTimeoutError = viewModel.onTimeoutError() - .test() - val onCheckIntervalError = viewModel.onCheckIntervalError() - .test() - val onSearchTermError = viewModel.onValidationSearchTermError() - .test() - val onScriptError = viewModel.onValidationScriptError() - .test() - - fillInModel().apply { - url.value = "" - } - val onDone = mock<() -> Unit>() - viewModel.commit(onDone) - - verify(validationManager, never()) - .scheduleValidation(any(), any(), any(), any()) - onNameError.assertNoValues() - onUrlError.assertValues(R.string.please_enter_url) - onTimeoutError.assertNoValues() - onCheckIntervalError.assertNoValues() - onSearchTermError.assertNoValues() - onScriptError.assertNoValues() - - verify(onDone, never()).invoke() - } - - @Test fun commit_urlFormatError() { - val onNameError = viewModel.onNameError() - .test() - val onUrlError = viewModel.onUrlError() - .test() - val onTimeoutError = viewModel.onTimeoutError() - .test() - val onCheckIntervalError = viewModel.onCheckIntervalError() - .test() - val onSearchTermError = viewModel.onValidationSearchTermError() - .test() - val onScriptError = viewModel.onValidationScriptError() - .test() - - fillInModel().apply { - url.value = "ftp://www.idk.com" - } - val onDone = mock<() -> Unit>() - viewModel.commit(onDone) - - verify(validationManager, never()) - .scheduleValidation(any(), any(), any(), any()) - onNameError.assertNoValues() - onUrlError.assertValues(R.string.please_enter_valid_url) - onTimeoutError.assertNoValues() - onCheckIntervalError.assertNoValues() - onSearchTermError.assertNoValues() - onScriptError.assertNoValues() - - verify(onDone, never()).invoke() - } - - @Test fun commit_networkTimeout_error() { - val onNameError = viewModel.onNameError() - .test() - val onUrlError = viewModel.onUrlError() - .test() - val onTimeoutError = viewModel.onTimeoutError() - .test() - val onCheckIntervalError = viewModel.onCheckIntervalError() - .test() - val onSearchTermError = viewModel.onValidationSearchTermError() - .test() - val onScriptError = viewModel.onValidationScriptError() - .test() - - fillInModel().apply { - timeout.value = 0 - } - val onDone = mock<() -> Unit>() - viewModel.commit(onDone) - - verify(validationManager, never()) - .scheduleValidation(any(), any(), any(), any()) - onNameError.assertNoValues() - onUrlError.assertNoValues() - onTimeoutError.assertValues(R.string.please_enter_networkTimeout) - onCheckIntervalError.assertNoValues() - onSearchTermError.assertNoValues() - onScriptError.assertNoValues() - - verify(onDone, never()).invoke() - } - - @Test fun commit_checkIntervalError() { - val onNameError = viewModel.onNameError() - .test() - val onUrlError = viewModel.onUrlError() - .test() - val onTimeoutError = viewModel.onTimeoutError() - .test() - val onCheckIntervalError = viewModel.onCheckIntervalError() - .test() - val onSearchTermError = viewModel.onValidationSearchTermError() - .test() - val onScriptError = viewModel.onValidationScriptError() - .test() - - fillInModel().apply { - checkIntervalValue.value = 0 - } - val onDone = mock<() -> Unit>() - viewModel.commit(onDone) - - verify(validationManager, never()) - .scheduleValidation(any(), any(), any(), any()) - onNameError.assertNoValues() - onUrlError.assertNoValues() - onTimeoutError.assertNoValues() - onCheckIntervalError.assertValues(R.string.please_enter_check_interval) - onSearchTermError.assertNoValues() - onScriptError.assertNoValues() - - verify(onDone, never()).invoke() - } - - @Test fun commit_termSearchError() { - val onNameError = viewModel.onNameError() - .test() - val onUrlError = viewModel.onUrlError() - .test() - val onTimeoutError = viewModel.onTimeoutError() - .test() - val onCheckIntervalError = viewModel.onCheckIntervalError() - .test() - val onSearchTermError = viewModel.onValidationSearchTermError() - .test() - val onScriptError = viewModel.onValidationScriptError() - .test() - - fillInModel().apply { - validationMode.value = TERM_SEARCH - validationSearchTerm.value = "" - } - val onDone = mock<() -> Unit>() - viewModel.commit(onDone) - - verify(validationManager, never()) - .scheduleValidation(any(), any(), any(), any()) - onNameError.assertNoValues() - onUrlError.assertNoValues() - onTimeoutError.assertNoValues() - onCheckIntervalError.assertNoValues() - onSearchTermError.assertValues(R.string.please_enter_search_term) - onScriptError.assertNoValues() - - verify(onDone, never()).invoke() - } - - @Test fun commit_javaScript_error() { - val onNameError = viewModel.onNameError() - .test() - val onUrlError = viewModel.onUrlError() - .test() - val onTimeoutError = viewModel.onTimeoutError() - .test() - val onCheckIntervalError = viewModel.onCheckIntervalError() - .test() - val onSearchTermError = viewModel.onValidationSearchTermError() - .test() - val onScriptError = viewModel.onValidationScriptError() - .test() - - fillInModel().apply { - validationMode.value = JAVASCRIPT - validationScript.value = "" - } - val onDone = mock<() -> Unit>() - viewModel.commit(onDone) - - verify(validationManager, never()) - .scheduleValidation(any(), any(), any(), any()) - onNameError.assertNoValues() - onUrlError.assertNoValues() - onTimeoutError.assertNoValues() - onCheckIntervalError.assertNoValues() - onSearchTermError.assertNoValues() - onScriptError.assertValues(R.string.please_enter_javaScript) - - verify(onDone, never()).invoke() - } - @Test fun commit_success() = runBlocking { val isLoading = viewModel.onIsLoading() .test() - val onNameError = viewModel.onNameError() - .test() - val onUrlError = viewModel.onUrlError() - .test() - val onTimeoutError = viewModel.onTimeoutError() - .test() - val onSearchTermError = viewModel.onValidationSearchTermError() - .test() - val onScriptError = viewModel.onValidationScriptError() - .test() - val onCheckIntervalError = viewModel.onCheckIntervalError() - .test() fillInModel() val onDone = mock<() -> Unit>() @@ -541,13 +302,6 @@ class ViewSiteViewModelTest { fromFinishingJob = false ) - onNameError.assertNoValues() - onUrlError.assertNoValues() - onTimeoutError.assertNoValues() - onCheckIntervalError.assertNoValues() - onSearchTermError.assertNoValues() - onScriptError.assertNoValues() - verify(onDone).invoke() }