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 5f39f37..26a21f9 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 @@ -28,7 +28,7 @@ import com.afollestad.nocknock.data.model.ValidationMode import com.afollestad.nocknock.ui.DarkModeSwitchActivity import com.afollestad.nocknock.ui.viewsite.KEY_SITE import com.afollestad.nocknock.utilities.ext.onTextChanged -import com.afollestad.nocknock.utilities.ext.toUri +import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection import com.afollestad.nocknock.utilities.livedata.distinct import com.afollestad.nocknock.viewcomponents.ext.dimenFloat import com.afollestad.nocknock.viewcomponents.ext.onScroll @@ -141,6 +141,13 @@ class AddSiteActivity : DarkModeSwitchActivity() { minutesData = viewModel.retryPolicyMinutes ) + // SSL certificate + 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) } @@ -179,9 +186,6 @@ class AddSiteActivity : DarkModeSwitchActivity() { } // SSL certificate - sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it.toUri() } - viewModel.certificateUri.distinct() - .observe(this, Observer { sslCertificateInput.setText(it.toString()) }) sslCertificateBrowse.setOnClickListener { val intent = Intent(ACTION_OPEN_DOCUMENT).apply { addCategory(CATEGORY_OPENABLE) @@ -193,12 +197,11 @@ class AddSiteActivity : DarkModeSwitchActivity() { override fun onResume() { super.onResume() - appToolbar.elevation = - if (scrollView.scrollY > appToolbar.measuredHeight / 2) { - appToolbar.dimenFloat(R.dimen.default_elevation) - } else { - 0f - } + appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) { + appToolbar.dimenFloat(R.dimen.default_elevation) + } else { + 0f + } } override fun onActivityResult( diff --git a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteViewModel.kt b/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteViewModel.kt index 3bb7113..87da65c 100644 --- a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteViewModel.kt +++ b/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteViewModel.kt @@ -15,7 +15,6 @@ */ package com.afollestad.nocknock.ui.addsite -import android.net.Uri import androidx.annotation.CheckResult import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting.PRIVATE @@ -40,6 +39,7 @@ import com.afollestad.nocknock.data.putSite import com.afollestad.nocknock.engine.validation.ValidationExecutor import com.afollestad.nocknock.ui.ScopedViewModel import com.afollestad.nocknock.utilities.ext.MINUTE +import com.afollestad.nocknock.utilities.ext.toUri import com.afollestad.nocknock.utilities.livedata.map import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan import kotlinx.coroutines.CoroutineDispatcher @@ -69,7 +69,7 @@ class AddSiteViewModel( val retryPolicyTimes = MutableLiveData() val retryPolicyMinutes = MutableLiveData() val headers = MutableLiveData>() - val certificateUri = MutableLiveData() + val certificateUri = MutableLiveData() @OnLifecycleEvent(ON_START) fun setDefaults() { @@ -91,6 +91,7 @@ class AddSiteViewModel( 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 @@ -130,6 +131,8 @@ class AddSiteViewModel( @CheckResult fun onCheckIntervalError(): LiveData = checkIntervalValueError + @CheckResult fun onCertificateError(): LiveData = certificateError + // Actions fun commit(done: () -> Unit) { scope.launch { @@ -228,6 +231,25 @@ class AddSiteViewModel( 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 } 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 3f83ea0..358dfd1 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 @@ -28,7 +28,7 @@ import com.afollestad.nocknock.data.model.Site import com.afollestad.nocknock.data.model.ValidationMode import com.afollestad.nocknock.ui.DarkModeSwitchActivity import com.afollestad.nocknock.utilities.ext.onTextChanged -import com.afollestad.nocknock.utilities.ext.toUri +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 @@ -158,6 +158,13 @@ class ViewSiteActivity : DarkModeSwitchActivity() { minutesData = viewModel.retryPolicyMinutes ) + // SSL certificate + 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) @@ -214,7 +221,7 @@ class ViewSiteActivity : DarkModeSwitchActivity() { .isVisible = it }) - // Done button + // Done item text viewModel.onDoneButtonText() .observe(this, Observer { toolbar.menu.findItem(R.id.commit) @@ -222,9 +229,6 @@ class ViewSiteActivity : DarkModeSwitchActivity() { }) // SSL certificate - sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it.toUri() } - viewModel.certificateUri.distinct() - .observe(this, Observer { sslCertificateInput.setText(it.toString()) }) sslCertificateBrowse.setOnClickListener { val intent = Intent(ACTION_OPEN_DOCUMENT).apply { addCategory(CATEGORY_OPENABLE) @@ -236,12 +240,11 @@ class ViewSiteActivity : DarkModeSwitchActivity() { override fun onResume() { super.onResume() - appToolbar.elevation = - if (scrollView.scrollY > appToolbar.measuredHeight / 2) { - appToolbar.dimenFloat(R.dimen.default_elevation) - } else { - 0f - } + appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) { + appToolbar.dimenFloat(R.dimen.default_elevation) + } else { + 0f + } } override fun onActivityResult( 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 531808d..91c3125 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 @@ -15,7 +15,6 @@ */ package com.afollestad.nocknock.ui.viewsite -import android.net.Uri import androidx.annotation.CheckResult import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting.PRIVATE @@ -24,9 +23,9 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.afollestad.nocknock.R import com.afollestad.nocknock.data.AppDatabase -import com.afollestad.nocknock.data.model.RetryPolicy import com.afollestad.nocknock.data.deleteSite import com.afollestad.nocknock.data.model.Header +import com.afollestad.nocknock.data.model.RetryPolicy import com.afollestad.nocknock.data.model.Site import com.afollestad.nocknock.data.model.Status import com.afollestad.nocknock.data.model.Status.WAITING @@ -41,6 +40,7 @@ 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 @@ -77,7 +77,7 @@ class ViewSiteViewModel( val retryPolicyTimes = MutableLiveData() val retryPolicyMinutes = MutableLiveData() val headers = MutableLiveData>() - val certificateUri = MutableLiveData() + val certificateUri = MutableLiveData() internal val disabled = MutableLiveData() internal val lastResult = MutableLiveData() @@ -89,6 +89,7 @@ class ViewSiteViewModel( 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 @@ -131,6 +132,8 @@ class ViewSiteViewModel( @CheckResult fun onDisableChecksVisibility(): LiveData = disabled.map { !it } + @CheckResult fun onCertificateError(): LiveData = certificateError + @CheckResult fun onDoneButtonText(): LiveData = disabled.map { if (it) R.string.renable_and_save_changes @@ -307,6 +310,25 @@ class ViewSiteViewModel( 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 } diff --git a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelExt.kt b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelExt.kt index 20623ca..3864984 100644 --- a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelExt.kt +++ b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelExt.kt @@ -24,7 +24,6 @@ import com.afollestad.nocknock.utilities.ext.DAY import com.afollestad.nocknock.utilities.ext.HOUR import com.afollestad.nocknock.utilities.ext.MINUTE import com.afollestad.nocknock.utilities.ext.WEEK -import com.afollestad.nocknock.utilities.ext.toUri import kotlin.math.ceil fun ViewSiteViewModel.setModel(site: Site) { @@ -56,9 +55,7 @@ fun ViewSiteViewModel.setModel(site: Site) { setCheckInterval(settings.validationIntervalMs) setRetryPolicy(site.retryPolicy) headers.value = site.headers - if (settings.certificate != null) { - certificateUri.value = settings.certificate!!.toUri() - } + certificateUri.value = settings.certificate this.disabled.value = settings.disabled this.lastResult.value = site.lastResult diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6f6e363..2b09271 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,6 +33,7 @@ Please input a search term. Please input a validation script. Please enter a network timeout greater than 0. + Certificate should be a valid file or content URI. Options Remove Site diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/ext/ViewExt.kt b/common/src/main/java/com/afollestad/nocknock/utilities/ext/ViewExt.kt index 86482e3..389de77 100644 --- a/common/src/main/java/com/afollestad/nocknock/utilities/ext/ViewExt.kt +++ b/common/src/main/java/com/afollestad/nocknock/utilities/ext/ViewExt.kt @@ -21,7 +21,12 @@ import android.widget.EditText import androidx.annotation.IntRange import kotlin.math.min -fun EditText.setTextAndMaintainSelection(text: CharSequence) { +fun EditText.setTextAndMaintainSelection(text: CharSequence?) { + if (text == null) { + setText("") + return + } + val formerStart = min(selectionStart, text.length) val formerEnd = min(selectionEnd, text.length) setText(text)