Compare commits
42 commits
Author | SHA1 | Date | |
---|---|---|---|
|
23ba4a69cd | ||
|
dd9aec1dff | ||
|
406af590aa | ||
|
550f8c59be | ||
|
10d7fe33f9 | ||
|
35eda8f057 | ||
|
a0fd44ae7a | ||
|
351f718df8 | ||
|
e2f7db22d1 | ||
|
82c1a17c68 | ||
|
a6670e2bea | ||
|
5fc1569099 | ||
|
0770db5df5 | ||
|
97a0eda92c | ||
|
1ccb89bfc3 | ||
|
9ea9c78099 | ||
|
997c797598 | ||
|
b26543d244 | ||
|
8c3654c4ac | ||
|
df2652860e | ||
|
4da8cb5f11 | ||
|
334e9e823c | ||
|
6d382b93a5 | ||
|
ef18464728 | ||
|
872e99d80d | ||
|
7f507792a8 | ||
|
68b6944542 | ||
|
e39093b526 | ||
|
9514a5ec83 | ||
|
3e5b1d4d8e | ||
|
de59bf9ec1 | ||
|
0fbd27b54b | ||
|
33388bd5c2 | ||
|
75297c7ff5 | ||
|
c6fca52fe4 | ||
|
b3f8a43f71 | ||
|
7dc4ee7fb1 | ||
|
859dcb53ca | ||
|
f86ccbbe0c | ||
|
571e7ebff3 | ||
|
77f939b095 | ||
|
8f16ff2d33 |
|
@ -1,3 +0,0 @@
|
|||
[*.kt]
|
||||
indent_size = 2
|
||||
continuation_indent_size=4
|
4
.gitignore
vendored
|
@ -180,4 +180,6 @@ gradle-app.setting
|
|||
.gradletasknamecache
|
||||
|
||||
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||
# gradle/wrapper/gradle-wrapper.properties
|
||||
# gradle/wrapper/gradle-wrapper.properties
|
||||
|
||||
app/google-services.json
|
2
.idea/misc.xml
generated
|
@ -40,7 +40,7 @@
|
|||
</value>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
|
22
.travis.yml
|
@ -1,22 +0,0 @@
|
|||
language: android
|
||||
jdk: oraclejdk8
|
||||
|
||||
env:
|
||||
matrix:
|
||||
- ANDROID_TARGET=android-21 ANDROID_ABI=armeabi-v7a
|
||||
|
||||
android:
|
||||
components:
|
||||
- tools # to get the new `repository-11.xml`
|
||||
- tools # see https://github.com/travis-ci/travis-ci/issues/6040#issuecomment-219367943)
|
||||
- platform-tools
|
||||
- build-tools-28.0.3
|
||||
- android-28
|
||||
|
||||
licenses:
|
||||
- '.+'
|
||||
|
||||
before_install:
|
||||
- yes | sdkmanager "platforms;android-28"
|
||||
script:
|
||||
- ./gradlew build connectedCheck
|
|
@ -1,6 +1,5 @@
|
|||
## Nock Nock
|
||||
|
||||
[](https://travis-ci.org/afollestad/nock-nock)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
|
||||

|
||||
|
|
|
@ -4,18 +4,6 @@ apply plugin: 'kotlin-android'
|
|||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
apply plugin: 'io.fabric'
|
||||
|
||||
def getFabricApiKey() {
|
||||
def propsFile = project.rootProject.file('local.properties')
|
||||
if (!propsFile.exists()) {
|
||||
return ""
|
||||
}
|
||||
Properties properties = new Properties()
|
||||
properties.load(propsFile.newDataInputStream())
|
||||
return properties.getProperty("fabric.apikey") ?: ""
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
buildToolsVersion versions.buildTools
|
||||
|
@ -26,17 +14,15 @@ android {
|
|||
targetSdkVersion versions.compileSdk
|
||||
versionCode versions.publishVersionCode
|
||||
versionName versions.publishVersion
|
||||
manifestPlaceholders = [fabricKey:getFabricApiKey()]
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
ext.enableCrashlytics = false
|
||||
buildConfigField "String", "FABRIC_API_KEY", "\"\""
|
||||
}
|
||||
release {
|
||||
buildConfigField "String", "FABRIC_API_KEY", "\"${getFabricApiKey()}\""
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility 1.8
|
||||
targetCompatibility 1.8
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/atomicfu.kotlin_module'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,6 +38,7 @@ dependencies {
|
|||
implementation 'androidx.recyclerview:recyclerview:' + versions.androidxRecyclerView
|
||||
implementation 'com.google.android.material:material:' + versions.googleMaterial
|
||||
implementation 'androidx.browser:browser:' + versions.androidxBrowser
|
||||
implementation 'com.google.firebase:firebase-core:' + versions.firebaseCore
|
||||
|
||||
// Lifecycle
|
||||
kapt 'androidx.lifecycle:lifecycle-compiler:' + versions.lifecycle
|
||||
|
@ -85,4 +72,8 @@ dependencies {
|
|||
androidTestImplementation 'androidx.test:rules:' + versions.androidxTestRunner
|
||||
}
|
||||
|
||||
apply from: '../spotless.gradle'
|
||||
apply from: '../spotless.gradle'
|
||||
apply from: '../mock/mock.gradle'
|
||||
|
||||
apply plugin: "io.fabric"
|
||||
apply plugin: 'com.google.gms.google-services'
|
|
@ -47,10 +47,8 @@ class NockNockApp : Application() {
|
|||
Timber.plant(DebugTree())
|
||||
}
|
||||
|
||||
if (BuildConfig.FABRIC_API_KEY.isNotEmpty()) {
|
||||
Timber.plant(FabricTree())
|
||||
Fabric.with(this, Crashlytics())
|
||||
}
|
||||
Timber.plant(FabricTree())
|
||||
Fabric.with(this, Crashlytics())
|
||||
|
||||
val modules = listOf(
|
||||
prefModule,
|
||||
|
|
|
@ -15,10 +15,15 @@
|
|||
*/
|
||||
package com.afollestad.nocknock.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.koin.PREF_DARK_MODE
|
||||
import com.afollestad.nocknock.ui.NightMode.DISABLED
|
||||
import com.afollestad.nocknock.ui.NightMode.ENABLED
|
||||
import com.afollestad.nocknock.ui.NightMode.UNKNOWN
|
||||
import com.afollestad.nocknock.utilities.rx.attachLifecycle
|
||||
import com.afollestad.rxkprefs.Pref
|
||||
import org.koin.android.ext.android.inject
|
||||
|
@ -35,16 +40,35 @@ abstract class DarkModeSwitchActivity : AppCompatActivity() {
|
|||
setTheme(themeRes())
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
darkModePref.observe()
|
||||
.filter { it != isDarkModeEnabled }
|
||||
.subscribe {
|
||||
log("Theme changed, recreating Activity.")
|
||||
recreate()
|
||||
}
|
||||
.attachLifecycle(this)
|
||||
if (getCurrentNightMode() == UNKNOWN) {
|
||||
darkModePref.observe()
|
||||
.filter { it != isDarkModeEnabled }
|
||||
.subscribe {
|
||||
log("Theme changed, recreating Activity.")
|
||||
recreate()
|
||||
}
|
||||
.attachLifecycle(this)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun isDarkMode() = darkModePref.get()
|
||||
protected fun getCurrentNightMode(): NightMode {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
return UNKNOWN
|
||||
}
|
||||
return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
|
||||
Configuration.UI_MODE_NIGHT_YES -> return ENABLED
|
||||
Configuration.UI_MODE_NIGHT_NO -> return DISABLED
|
||||
else -> UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
protected fun isDarkMode(): Boolean {
|
||||
return when (getCurrentNightMode()) {
|
||||
ENABLED -> true
|
||||
DISABLED -> false
|
||||
else -> darkModePref.get()
|
||||
}
|
||||
}
|
||||
|
||||
protected fun toggleDarkMode() = setDarkMode(!isDarkMode())
|
||||
|
||||
|
|
26
app/src/main/java/com/afollestad/nocknock/ui/NightMode.kt
Normal file
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.ui
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
enum class NightMode {
|
||||
/** Night mode is on at the system level. */
|
||||
ENABLED,
|
||||
/** Night mode is off at the system level. */
|
||||
DISABLED,
|
||||
/** We don't know about night mode, fallback to custom impl. */
|
||||
UNKNOWN
|
||||
}
|
|
@ -31,11 +31,13 @@ import com.afollestad.nocknock.utilities.ext.onTextChanged
|
|||
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.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_addsite.checkIntervalLayout
|
||||
import kotlinx.android.synthetic.main.activity_addsite.headersLayout
|
||||
import kotlinx.android.synthetic.main.activity_addsite.inputName
|
||||
|
@ -64,12 +66,14 @@ class AddSiteActivity : DarkModeSwitchActivity() {
|
|||
}
|
||||
|
||||
private val viewModel by viewModel<AddSiteViewModel>()
|
||||
private lateinit var validationForm: Form
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_addsite)
|
||||
setupUi()
|
||||
setupValidation()
|
||||
|
||||
lifecycle.addObserver(viewModel)
|
||||
|
||||
|
@ -82,23 +86,17 @@ class AddSiteActivity : 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(
|
||||
|
@ -107,8 +105,6 @@ class AddSiteActivity : DarkModeSwitchActivity() {
|
|||
outTransformer = { ValidationMode.fromIndex(it) },
|
||||
inTransformer = { it.toIndex() }
|
||||
)
|
||||
viewModel.onValidationSearchTermError()
|
||||
.toViewError(this, responseValidationSearchTerm)
|
||||
viewModel.onValidationModeDescription()
|
||||
.toViewText(this, validationModeDescription)
|
||||
|
||||
|
@ -121,32 +117,10 @@ class AddSiteActivity : DarkModeSwitchActivity() {
|
|||
viewModel.onValidationSearchTermVisibility()
|
||||
.toViewVisibility(this, responseValidationSearchTerm)
|
||||
|
||||
// Validation script
|
||||
scriptInputLayout.attach(
|
||||
codeData = viewModel.validationScript,
|
||||
errorData = viewModel.onValidationScriptError(),
|
||||
visibility = viewModel.onValidationScriptVisibility()
|
||||
)
|
||||
|
||||
// Check interval
|
||||
checkIntervalLayout.attach(
|
||||
valueData = viewModel.checkIntervalValue,
|
||||
multiplierData = viewModel.checkIntervalUnit,
|
||||
errorData = viewModel.onCheckIntervalError()
|
||||
)
|
||||
|
||||
// Retry Policy
|
||||
retryPolicyLayout.attach(
|
||||
timesData = viewModel.retryPolicyTimes,
|
||||
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)
|
||||
|
@ -156,15 +130,6 @@ class AddSiteActivity : DarkModeSwitchActivity() {
|
|||
toolbarTitle.setText(R.string.add_site)
|
||||
toolbar.run {
|
||||
inflateMenu(R.menu.menu_addsite)
|
||||
setOnMenuItemClickListener {
|
||||
if (it.itemId == R.id.commit) {
|
||||
viewModel.commit {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
setNavigationIcon(R.drawable.ic_action_close)
|
||||
setNavigationOnClickListener { finish() }
|
||||
}
|
||||
|
@ -195,6 +160,59 @@ class AddSiteActivity : 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", optional = true) {
|
||||
isUri().hasScheme("file", "content")
|
||||
.that { it.host != null }
|
||||
.description(R.string.please_enter_validCertUri)
|
||||
}
|
||||
submitWith(toolbar.menu, R.id.commit) {
|
||||
viewModel.commit {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validation script
|
||||
scriptInputLayout.attach(
|
||||
codeData = viewModel.validationScript,
|
||||
visibility = viewModel.onValidationScriptVisibility(),
|
||||
form = validationForm
|
||||
)
|
||||
|
||||
// Check interval
|
||||
checkIntervalLayout.attach(
|
||||
valueData = viewModel.checkIntervalValue,
|
||||
multiplierData = viewModel.checkIntervalUnit,
|
||||
form = validationForm
|
||||
)
|
||||
|
||||
// Retry Policy
|
||||
retryPolicyLayout.attach(
|
||||
timesData = viewModel.retryPolicyTimes,
|
||||
minutesData = viewModel.retryPolicyMinutes,
|
||||
form = validationForm
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {
|
||||
|
|
|
@ -39,9 +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
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -83,23 +81,10 @@ class AddSiteViewModel(
|
|||
headers.value = emptyList()
|
||||
}
|
||||
|
||||
// Private properties
|
||||
private val isLoading = MutableLiveData<Boolean>()
|
||||
private val nameError = MutableLiveData<Int?>()
|
||||
private val urlError = MutableLiveData<Int?>()
|
||||
private val timeoutError = MutableLiveData<Int?>()
|
||||
private val validationSearchTermError = MutableLiveData<Int?>()
|
||||
private val validationScriptError = MutableLiveData<Int?>()
|
||||
private val checkIntervalValueError = MutableLiveData<Int?>()
|
||||
private val certificateError = MutableLiveData<Int?>()
|
||||
|
||||
// Expose private properties or calculated properties
|
||||
@CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading
|
||||
|
||||
@CheckResult fun onNameError(): LiveData<Int?> = nameError
|
||||
|
||||
@CheckResult fun onUrlError(): LiveData<Int?> = urlError
|
||||
|
||||
@CheckResult fun onUrlWarningVisibility(): LiveData<Boolean> {
|
||||
return url.map {
|
||||
val parsed = HttpUrl.parse(it)
|
||||
|
@ -107,8 +92,6 @@ class AddSiteViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
@CheckResult fun onTimeoutError(): LiveData<Int?> = timeoutError
|
||||
|
||||
@CheckResult fun onValidationModeDescription(): LiveData<Int> {
|
||||
return validationMode.map {
|
||||
when (it!!) {
|
||||
|
@ -119,19 +102,9 @@ class AddSiteViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
@CheckResult fun onValidationSearchTermError(): LiveData<Int?> = validationSearchTermError
|
||||
@CheckResult fun onValidationSearchTermVisibility() = validationMode.map { it == TERM_SEARCH }
|
||||
|
||||
@CheckResult fun onValidationSearchTermVisibility() =
|
||||
validationMode.map { it == TERM_SEARCH }
|
||||
|
||||
@CheckResult fun onValidationScriptError(): LiveData<Int?> = validationScriptError
|
||||
|
||||
@CheckResult fun onValidationScriptVisibility() =
|
||||
validationMode.map { it == JAVASCRIPT }
|
||||
|
||||
@CheckResult fun onCheckIntervalError(): LiveData<Int?> = checkIntervalValueError
|
||||
|
||||
@CheckResult fun onCertificateError(): LiveData<Int?> = certificateError
|
||||
@CheckResult fun onValidationScriptVisibility() = validationMode.map { it == JAVASCRIPT }
|
||||
|
||||
// Actions
|
||||
fun commit(done: () -> Unit) {
|
||||
|
@ -171,89 +144,7 @@ class AddSiteViewModel(
|
|||
}
|
||||
|
||||
private fun generateDbModel(): 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 = SiteSettings(
|
||||
|
@ -262,7 +153,7 @@ class AddSiteViewModel(
|
|||
validationArgs = getValidationArgs(),
|
||||
networkTimeout = timeout,
|
||||
disabled = false,
|
||||
certificate = certificateUri.value.toString()
|
||||
certificate = certificateUri.value?.toString()
|
||||
)
|
||||
|
||||
val newLastResult = ValidationResult(
|
||||
|
@ -275,7 +166,8 @@ class AddSiteViewModel(
|
|||
val retryPolicyMinutes = retryPolicyMinutes.value ?: 0
|
||||
val newRetryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) {
|
||||
RetryPolicy(
|
||||
count = retryPolicyTimes, minutes = retryPolicyMinutes
|
||||
count = retryPolicyTimes,
|
||||
minutes = retryPolicyMinutes
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
|
|
@ -24,7 +24,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.list.listItems
|
||||
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.adapter.SiteAdapter
|
||||
import com.afollestad.nocknock.adapter.TagAdapter
|
||||
|
@ -33,10 +32,8 @@ import com.afollestad.nocknock.data.model.Site
|
|||
import com.afollestad.nocknock.dialogs.AboutDialog
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
||||
import com.afollestad.nocknock.ui.NightMode.UNKNOWN
|
||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||
import com.afollestad.nocknock.utilities.ui.toast
|
||||
import com.afollestad.nocknock.viewUrl
|
||||
import com.afollestad.nocknock.viewUrlWithApp
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||
import kotlinx.android.synthetic.main.activity_main.fab
|
||||
import kotlinx.android.synthetic.main.activity_main.list
|
||||
|
@ -93,12 +90,17 @@ class MainActivity : DarkModeSwitchActivity() {
|
|||
toolbar.run {
|
||||
inflateMenu(R.menu.menu_main)
|
||||
menu.findItem(R.id.dark_mode)
|
||||
.isChecked = isDarkMode()
|
||||
.apply {
|
||||
if (getCurrentNightMode() == UNKNOWN) {
|
||||
isChecked = isDarkMode()
|
||||
} else {
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.about -> AboutDialog.show(this@MainActivity)
|
||||
R.id.dark_mode -> toggleDarkMode()
|
||||
R.id.support_me -> supportMe()
|
||||
}
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
|
@ -144,20 +146,4 @@ class MainActivity : DarkModeSwitchActivity() {
|
|||
viewSite(model)
|
||||
}
|
||||
}
|
||||
|
||||
private fun supportMe() {
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.support_me)
|
||||
message(R.string.support_me_message, html = true, lineHeightMultiplier = 1.4f)
|
||||
listItemsSingleChoice(R.array.donation_options) { _, index, _ ->
|
||||
when (index) {
|
||||
0 -> viewUrl("https://paypal.me/AidanFollestad")
|
||||
1 -> viewUrlWithApp("https://cash.me/\$afollestad", pkg = "com.squareup.cash")
|
||||
2 -> viewUrlWithApp("https://venmo.com/afollestad", pkg = "com.venmo")
|
||||
}
|
||||
toast(R.string.thank_you)
|
||||
}
|
||||
positiveButton(R.string.next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ViewSiteViewModel>()
|
||||
private lateinit var validationForm: Form
|
||||
|
||||
private val intentProvider by inject<IntentProvider>()
|
||||
private val statusUpdateReceiver by lazy {
|
||||
|
@ -81,17 +84,18 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_viewsite)
|
||||
setupUi()
|
||||
|
||||
lifecycle.run {
|
||||
addObserver(viewModel)
|
||||
addObserver(statusUpdateReceiver)
|
||||
}
|
||||
|
||||
// Populate view model with initial data
|
||||
val model = intent.getSerializableExtra(KEY_SITE) as Site
|
||||
viewModel.setModel(model)
|
||||
|
||||
setupUi()
|
||||
setupValidation()
|
||||
lifecycle.run {
|
||||
addObserver(viewModel)
|
||||
addObserver(statusUpdateReceiver)
|
||||
}
|
||||
|
||||
// Loading
|
||||
loadingProgress.observe(this, viewModel.onIsLoading())
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -138,32 +134,10 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
|
|||
viewModel.onValidationSearchTermVisibility()
|
||||
.toViewVisibility(this, responseValidationSearchTerm)
|
||||
|
||||
// Validation script
|
||||
scriptInputLayout.attach(
|
||||
codeData = viewModel.validationScript,
|
||||
errorData = viewModel.onValidationScriptError(),
|
||||
visibility = viewModel.onValidationScriptVisibility()
|
||||
)
|
||||
|
||||
// Check interval
|
||||
checkIntervalLayout.attach(
|
||||
valueData = viewModel.checkIntervalValue,
|
||||
multiplierData = viewModel.checkIntervalUnit,
|
||||
errorData = viewModel.onCheckIntervalError()
|
||||
)
|
||||
|
||||
// Retry Policy
|
||||
retryPolicyLayout.attach(
|
||||
timesData = viewModel.retryPolicyTimes,
|
||||
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)
|
||||
|
@ -190,7 +164,6 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
|
|||
|
||||
setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.commit -> viewModel.commit { finish() }
|
||||
R.id.remove -> maybeRemoveSite()
|
||||
R.id.disableChecks -> maybeDisableChecks()
|
||||
}
|
||||
|
@ -238,6 +211,56 @@ 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(responseValidationSearchTerm, name = "Search term") {
|
||||
conditional(responseValidationSearchTerm.isVisibleCondition()) {
|
||||
isNotEmpty().description(R.string.please_enter_search_term)
|
||||
}
|
||||
}
|
||||
input(responseTimeoutInput, name = "Timeout", optional = true) {
|
||||
isNumber().greaterThan(0)
|
||||
.description(R.string.please_enter_networkTimeout)
|
||||
}
|
||||
input(sslCertificateInput, name = "Certificate Path", optional = true) {
|
||||
isUri().hasScheme("file", "content")
|
||||
.that { it.host != null }
|
||||
.description(R.string.please_enter_validCertUri)
|
||||
}
|
||||
submitWith(toolbar.menu, R.id.commit) {
|
||||
viewModel.commit { finish() }
|
||||
}
|
||||
}
|
||||
|
||||
// Validation script
|
||||
scriptInputLayout.attach(
|
||||
codeData = viewModel.validationScript,
|
||||
visibility = viewModel.onValidationScriptVisibility(),
|
||||
form = validationForm
|
||||
)
|
||||
|
||||
// Check interval
|
||||
checkIntervalLayout.attach(
|
||||
valueData = viewModel.checkIntervalValue,
|
||||
multiplierData = viewModel.checkIntervalUnit,
|
||||
form = validationForm
|
||||
)
|
||||
|
||||
// Retry Policy
|
||||
retryPolicyLayout.attach(
|
||||
timesData = viewModel.retryPolicyTimes,
|
||||
minutesData = viewModel.retryPolicyMinutes,
|
||||
form = validationForm
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {
|
||||
|
|
|
@ -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<Boolean>()
|
||||
internal val lastResult = MutableLiveData<ValidationResult?>()
|
||||
|
||||
// Private properties
|
||||
private val isLoading = MutableLiveData<Boolean>()
|
||||
private val nameError = MutableLiveData<Int?>()
|
||||
private val urlError = MutableLiveData<Int?>()
|
||||
private val timeoutError = MutableLiveData<Int?>()
|
||||
private val validationSearchTermError = MutableLiveData<Int?>()
|
||||
private val validationScriptError = MutableLiveData<Int?>()
|
||||
private val checkIntervalValueError = MutableLiveData<Int?>()
|
||||
private val certificateError = MutableLiveData<Int?>()
|
||||
|
||||
// Expose private properties or calculated properties
|
||||
@CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading
|
||||
|
||||
@CheckResult fun onNameError(): LiveData<Int?> = nameError
|
||||
|
||||
@CheckResult fun onUrlError(): LiveData<Int?> = urlError
|
||||
|
||||
@CheckResult fun onUrlWarningVisibility(): LiveData<Boolean> {
|
||||
return url.map {
|
||||
val parsed = HttpUrl.parse(it)
|
||||
|
@ -105,8 +90,6 @@ class ViewSiteViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
@CheckResult fun onTimeoutError(): LiveData<Int?> = timeoutError
|
||||
|
||||
@CheckResult fun onValidationModeDescription(): LiveData<Int> {
|
||||
return validationMode.map {
|
||||
when (it!!) {
|
||||
|
@ -117,22 +100,11 @@ class ViewSiteViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
@CheckResult fun onValidationSearchTermError(): LiveData<Int?> = 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<Int?> = validationScriptError
|
||||
|
||||
@CheckResult fun onValidationScriptVisibility() =
|
||||
validationMode.map { it == JAVASCRIPT }
|
||||
|
||||
@CheckResult fun onCheckIntervalError(): LiveData<Int?> = checkIntervalValueError
|
||||
|
||||
@CheckResult fun onDisableChecksVisibility(): LiveData<Boolean> =
|
||||
disabled.map { !it }
|
||||
|
||||
@CheckResult fun onCertificateError(): LiveData<Int?> = certificateError
|
||||
@CheckResult fun onDisableChecksVisibility(): LiveData<Boolean> = disabled.map { !it }
|
||||
|
||||
@CheckResult fun onDoneButtonText(): LiveData<Int> =
|
||||
disabled.map {
|
||||
|
@ -250,90 +222,8 @@ 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 cleanedTags = tags.value?.split(',')?.joinToString(separator = ",") ?: ""
|
||||
|
||||
val newSettings = site.settings!!.copy(
|
||||
validationIntervalMs = getCheckIntervalMs(),
|
||||
|
@ -341,7 +231,7 @@ class ViewSiteViewModel(
|
|||
validationArgs = getValidationArgs(),
|
||||
networkTimeout = timeout,
|
||||
disabled = false,
|
||||
certificate = certificateUri.value.toString()
|
||||
certificate = certificateUri.value?.toString()
|
||||
)
|
||||
|
||||
val retryPolicyTimes = retryPolicyTimes.value ?: 0
|
||||
|
@ -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 {
|
||||
|
|
|
@ -55,7 +55,11 @@ fun ViewSiteViewModel.setModel(site: Site) {
|
|||
setCheckInterval(settings.validationIntervalMs)
|
||||
setRetryPolicy(site.retryPolicy)
|
||||
headers.value = site.headers
|
||||
certificateUri.value = settings.certificate
|
||||
if (settings.certificate == "null") {
|
||||
certificateUri.value = ""
|
||||
} else {
|
||||
certificateUri.value = settings.certificate
|
||||
}
|
||||
|
||||
this.disabled.value = settings.disabled
|
||||
this.lastResult.value = site.lastResult
|
||||
|
@ -65,22 +69,22 @@ private fun ViewSiteViewModel.setCheckInterval(interval: Long) {
|
|||
when {
|
||||
interval >= WEEK -> {
|
||||
checkIntervalValue.value =
|
||||
getIntervalFromUnit(interval, WEEK)
|
||||
getIntervalFromUnit(interval, WEEK)
|
||||
checkIntervalUnit.value = WEEK
|
||||
}
|
||||
interval >= DAY -> {
|
||||
checkIntervalValue.value =
|
||||
getIntervalFromUnit(interval, DAY)
|
||||
getIntervalFromUnit(interval, DAY)
|
||||
checkIntervalUnit.value = DAY
|
||||
}
|
||||
interval >= HOUR -> {
|
||||
checkIntervalValue.value =
|
||||
getIntervalFromUnit(interval, HOUR)
|
||||
getIntervalFromUnit(interval, HOUR)
|
||||
checkIntervalUnit.value = HOUR
|
||||
}
|
||||
interval >= MINUTE -> {
|
||||
checkIntervalValue.value =
|
||||
getIntervalFromUnit(interval, MINUTE)
|
||||
getIntervalFromUnit(interval, MINUTE)
|
||||
checkIntervalUnit.value = MINUTE
|
||||
}
|
||||
else -> {
|
||||
|
|
|
@ -123,7 +123,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/content_inset"
|
||||
android:layout_marginTop="@dimen/content_inset_half"
|
||||
android:background="@color/lighterGray"
|
||||
android:background="?scriptLayoutBackground"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
|
|
|
@ -161,7 +161,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/content_inset"
|
||||
android:layout_marginTop="@dimen/content_inset_half"
|
||||
android:background="@color/lighterGray"
|
||||
android:background="?scriptLayoutBackground"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
|
|
|
@ -7,7 +7,4 @@
|
|||
android:id="@+id/dark_mode"
|
||||
android:checkable="true"
|
||||
android:title="@string/dark_mode"/>
|
||||
<item
|
||||
android:id="@+id/support_me"
|
||||
android:title="@string/support_me"/>
|
||||
</menu>
|
||||
|
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 5.4 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 7.7 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 15 KiB |
|
@ -13,10 +13,4 @@
|
|||
<item>JavaScript Evaluation</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="donation_options">
|
||||
<item>via PayPal</item>
|
||||
<item>via Cash App</item>
|
||||
<item>via Venmo</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -4,5 +4,6 @@
|
|||
<attr format="color" name="toolbarTitleColor"/>
|
||||
<attr format="color" name="dividerColor"/>
|
||||
<attr format="color" name="iconColor"/>
|
||||
<attr format="color" name="scriptLayoutBackground"/>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
<color name="colorPrimary_darkTheme">#212121</color>
|
||||
<color name="colorPrimaryDark_darkTheme">#252525</color>
|
||||
|
||||
<color name="lighterGray">#303030</color>
|
||||
<color name="darkerGray">#303030</color>
|
||||
<color name="lighterGray">#EEEEEE</color>
|
||||
|
||||
<color name="colorAccent">#FF6E40</color>
|
||||
<color name="colorAccent_pressed">#E44615</color>
|
||||
<color name="colorAccent_translucent">#40FF6E40</color>
|
||||
|
|
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#758F9A</color>
|
||||
</resources>
|
|
@ -14,6 +14,7 @@
|
|||
<a href=\'https://www.linkedin.com/in/afollestad\'>LinkedIn</a>
|
||||
<br/><br/><i>Nock Nock is open source! Check out the <a href=\'https://github.com/afollestad/nock-nock\'>GitHub page</a>!</i>
|
||||
<br/>Icon by <a href=\'https://plus.google.com/+KevinAguilarC\'>Kevin Aguilar</a> of <b>221 Pixels</b>.
|
||||
<br/>View the <a href=\'https://af.codes/privacypolicies/nocknock.html\'>Privacy Policy</a>.
|
||||
]]></string>
|
||||
<string name="dark_mode">Dark Mode</string>
|
||||
|
||||
|
@ -29,9 +30,7 @@
|
|||
<string name="please_enter_name">Please enter a name!</string>
|
||||
<string name="please_enter_url">Please enter a URL.</string>
|
||||
<string name="please_enter_valid_url">Please enter a valid URL.</string>
|
||||
<string name="please_enter_check_interval">Please input a validation interval.</string>
|
||||
<string name="please_enter_search_term">Please input a search term.</string>
|
||||
<string name="please_enter_javaScript">Please input a validation script.</string>
|
||||
<string name="please_enter_networkTimeout">Please enter a network timeout greater than 0.</string>
|
||||
<string name="please_enter_validCertUri">Certificate should be a valid file or content URI.</string>
|
||||
|
||||
|
@ -86,14 +85,6 @@
|
|||
exception to pass custom error messages to Nock Nock.
|
||||
</string>
|
||||
|
||||
<string name="support_me">Donate</string>
|
||||
<string name="support_me_message"><![CDATA[
|
||||
<b>Nock Nock</b> was created and is maintained by one person. Donations are <b>much</b>
|
||||
appreciated and encourage continued support.
|
||||
]]></string>
|
||||
<string name="thank_you">Thank you very much!</string>
|
||||
<string name="next">Next</string>
|
||||
|
||||
<string name="install_web_browser">Please install a web browser app, such as Google Chrome.</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<style name="PrimaryDarkButton" parent="Widget.MaterialComponents.Button">
|
||||
<item name="android:textColor">#fff</item>
|
||||
<item name="backgroundTint">@color/lighterGray</item>
|
||||
<item name="backgroundTint">@color/darkerGray</item>
|
||||
<item name="android:fontFamily">@font/lato</item>
|
||||
</style>
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<item name="toolbarTitleColor">#000000</item>
|
||||
<item name="dividerColor">#EEEEEE</item>
|
||||
<item name="iconColor">#000000</item>
|
||||
<item name="scriptLayoutBackground">@color/lighterGray</item>
|
||||
|
||||
<item name="android:textColorPrimary">#212121</item>
|
||||
<item name="android:textColorSecondary">#727272</item>
|
||||
|
@ -33,6 +34,7 @@
|
|||
<item name="toolbarTitleColor">#ffffff</item>
|
||||
<item name="dividerColor">#303030</item>
|
||||
<item name="iconColor">#FFFFFF</item>
|
||||
<item name="scriptLayoutBackground">@color/darkerGray</item>
|
||||
|
||||
<item name="android:textColorPrimary">#FFFFFF</item>
|
||||
<item name="android:textColorSecondary">#F0F0F0</item>
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.afollestad.nocknock">
|
||||
|
||||
<application>
|
||||
|
||||
<meta-data
|
||||
android:name="io.fabric.ApiKey"
|
||||
android:value="${fabricKey}"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -17,20 +17,21 @@ package com.afollestad.nocknock.ui.addsite
|
|||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.model.Header
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.SiteSettings
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.model.ValidationResult
|
||||
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||
import com.afollestad.nocknock.mockDatabase
|
||||
import com.afollestad.nocknock.utilities.ext.MINUTE
|
||||
import com.afollestad.nocknock.utilities.livedata.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import com.nhaarman.mockitokotlin2.argumentCaptor
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import com.nhaarman.mockitokotlin2.never
|
||||
import com.nhaarman.mockitokotlin2.verify
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
@ -149,247 +150,9 @@ class AddSiteViewModelTest {
|
|||
assertThat(viewModel.getValidationArgs()).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>()
|
||||
|
@ -397,31 +160,30 @@ class AddSiteViewModelTest {
|
|||
|
||||
val siteCaptor = argumentCaptor<Site>()
|
||||
val settingsCaptor = argumentCaptor<SiteSettings>()
|
||||
val validationResultCaptor = argumentCaptor<ValidationResult>()
|
||||
|
||||
isLoading.assertValues(true, false)
|
||||
verify(database.siteDao()).insert(siteCaptor.capture())
|
||||
verify(database.siteSettingsDao()).insert(settingsCaptor.capture())
|
||||
verify(database.validationResultsDao(), never()).insert(any())
|
||||
verify(database.validationResultsDao()).insert(validationResultCaptor.capture())
|
||||
|
||||
val settings = settingsCaptor.firstValue
|
||||
val result = validationResultCaptor.firstValue.copy(siteId = 1)
|
||||
val model = siteCaptor.firstValue.copy(
|
||||
id = 1, // fill it in because our insert captor doesn't catch this
|
||||
settings = settings,
|
||||
lastResult = null
|
||||
lastResult = result
|
||||
)
|
||||
|
||||
assertThat(result.reason).isNull()
|
||||
assertThat(result.status).isEqualTo(WAITING)
|
||||
|
||||
verify(validationManager).scheduleValidation(
|
||||
site = model,
|
||||
rightNow = true,
|
||||
cancelPrevious = true,
|
||||
fromFinishingJob = false
|
||||
)
|
||||
onNameError.assertNoValues()
|
||||
onUrlError.assertNoValues()
|
||||
onTimeoutError.assertNoValues()
|
||||
onCheckIntervalError.assertNoValues()
|
||||
onSearchTermError.assertNoValues()
|
||||
onScriptError.assertNoValues()
|
||||
|
||||
verify(onDone).invoke()
|
||||
}
|
||||
|
@ -435,5 +197,10 @@ class AddSiteViewModelTest {
|
|||
validationScript.value = null
|
||||
checkIntervalValue.value = 60
|
||||
checkIntervalUnit.value = 1000
|
||||
tags.value = "one,two"
|
||||
headers.value = listOf(
|
||||
Header(2L, 1L, key = "Content-Type", value = "text/html"),
|
||||
Header(3L, 1L, key = "User-Agent", value = "NockNock")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@ package com.afollestad.nocknock.ui.viewsite
|
|||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.afollestad.nocknock.MOCK_MODEL_1
|
||||
import com.afollestad.nocknock.R
|
||||
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.SiteSettings
|
||||
import com.afollestad.nocknock.data.model.Status.CHECKING
|
||||
|
@ -29,6 +31,7 @@ import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
|||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.model.ValidationResult
|
||||
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||
import com.afollestad.nocknock.fakeRetryPolicy
|
||||
import com.afollestad.nocknock.mockDatabase
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.utilities.livedata.test
|
||||
|
@ -38,9 +41,10 @@ import com.google.common.truth.Truth.assertThat
|
|||
import com.nhaarman.mockitokotlin2.any
|
||||
import com.nhaarman.mockitokotlin2.argumentCaptor
|
||||
import com.nhaarman.mockitokotlin2.doAnswer
|
||||
import com.nhaarman.mockitokotlin2.doReturn
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import com.nhaarman.mockitokotlin2.never
|
||||
import com.nhaarman.mockitokotlin2.verify
|
||||
import com.nhaarman.mockitokotlin2.whenever
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
@ -255,247 +259,11 @@ 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 {
|
||||
whenever(database.retryPolicyDao().forSite(any())).doReturn(listOf(fakeRetryPolicy(1)))
|
||||
|
||||
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>()
|
||||
|
@ -506,11 +274,13 @@ class ViewSiteViewModelTest {
|
|||
val siteCaptor = argumentCaptor<Site>()
|
||||
val settingsCaptor = argumentCaptor<SiteSettings>()
|
||||
val resultCaptor = argumentCaptor<ValidationResult>()
|
||||
val retryPolicyCaptor = argumentCaptor<RetryPolicy>()
|
||||
|
||||
isLoading.assertValues(true, false)
|
||||
verify(database.siteDao()).update(siteCaptor.capture())
|
||||
verify(database.siteSettingsDao()).update(settingsCaptor.capture())
|
||||
verify(database.validationResultsDao()).update(resultCaptor.capture())
|
||||
verify(database.retryPolicyDao()).update(retryPolicyCaptor.capture())
|
||||
|
||||
// From fillInModel() below
|
||||
val updatedSettings = MOCK_MODEL_1.settings!!.copy(
|
||||
|
@ -523,11 +293,13 @@ class ViewSiteViewModelTest {
|
|||
val updatedResult = MOCK_MODEL_1.lastResult!!.copy(
|
||||
status = WAITING
|
||||
)
|
||||
val retryPolicy = retryPolicyCaptor.firstValue
|
||||
val updatedModel = MOCK_MODEL_1.copy(
|
||||
name = "Hello There",
|
||||
url = "https://www.hellothere.com",
|
||||
settings = updatedSettings,
|
||||
lastResult = updatedResult
|
||||
lastResult = updatedResult,
|
||||
retryPolicy = retryPolicy
|
||||
)
|
||||
|
||||
assertThat(siteCaptor.firstValue).isEqualTo(updatedModel)
|
||||
|
@ -541,13 +313,6 @@ class ViewSiteViewModelTest {
|
|||
fromFinishingJob = false
|
||||
)
|
||||
|
||||
onNameError.assertNoValues()
|
||||
onUrlError.assertNoValues()
|
||||
onTimeoutError.assertNoValues()
|
||||
onCheckIntervalError.assertNoValues()
|
||||
onSearchTermError.assertNoValues()
|
||||
onScriptError.assertNoValues()
|
||||
|
||||
verify(onDone).invoke()
|
||||
}
|
||||
|
||||
|
@ -619,5 +384,12 @@ class ViewSiteViewModelTest {
|
|||
validationScript.value = "throw 'Oh no!'"
|
||||
checkIntervalValue.value = 24
|
||||
checkIntervalUnit.value = 60000
|
||||
tags.value = "one,two"
|
||||
retryPolicyTimes.value = 5
|
||||
retryPolicyMinutes.value = 5
|
||||
headers.value = listOf(
|
||||
Header(2L, 1L, key = "Content-Type", value = "text/html"),
|
||||
Header(3L, 1L, key = "User-Agent", value = "NockNock")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ buildscript {
|
|||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:' + versions.kotlin
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:' + versions.versionPlugin
|
||||
classpath 'io.fabric.tools:gradle:' + versions.fabricPlugin
|
||||
classpath 'com.google.gms:google-services:' + versions.googleServices
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,7 +23,6 @@ allprojects {
|
|||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url "https://dl.bintray.com/drummer-aidan/maven" }
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,10 @@ android {
|
|||
versionName versions.publishVersion
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/atomicfu.kotlin_module'
|
||||
}
|
||||
|
||||
// For Mozilla Rhino
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
|
@ -30,6 +34,7 @@ dependencies {
|
|||
implementation 'org.mozilla:rhino:' + versions.rhino
|
||||
|
||||
api 'com.afollestad:rxkprefs:' + versions.rxkPrefs
|
||||
api "io.reactivex.rxjava2:rxjava:" + versions.rxJava
|
||||
|
||||
testImplementation 'junit:junit:' + versions.junit
|
||||
testImplementation 'com.google.truth:truth:' + versions.truth
|
||||
|
|
|
@ -18,3 +18,10 @@ package com.afollestad.nocknock.utilities.ext
|
|||
import android.net.Uri
|
||||
|
||||
fun String.toUri() = Uri.parse(this)!!
|
||||
|
||||
fun String?.isNotNullOrEmpty(): Boolean {
|
||||
if (this == null || this == "null") {
|
||||
return false
|
||||
}
|
||||
return !isNullOrEmpty()
|
||||
}
|
||||
|
|
|
@ -14,6 +14,10 @@ android {
|
|||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/atomicfu.kotlin_module'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
@ -181,7 +181,8 @@ class AppDatabaseTest() {
|
|||
validationMode = STATUS_CODE,
|
||||
validationArgs = null,
|
||||
disabled = false,
|
||||
networkTimeout = 10000
|
||||
networkTimeout = 10000,
|
||||
certificate = null
|
||||
)
|
||||
val newId = settingsDao.insert(model)
|
||||
assertThat(newId).isEqualTo(1)
|
||||
|
@ -199,7 +200,8 @@ class AppDatabaseTest() {
|
|||
validationMode = STATUS_CODE,
|
||||
validationArgs = null,
|
||||
disabled = false,
|
||||
networkTimeout = 10000
|
||||
networkTimeout = 10000,
|
||||
certificate = null
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -227,7 +229,8 @@ class AppDatabaseTest() {
|
|||
validationMode = STATUS_CODE,
|
||||
validationArgs = null,
|
||||
disabled = false,
|
||||
networkTimeout = 10000
|
||||
networkTimeout = 10000,
|
||||
certificate = null
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -428,9 +431,30 @@ class AppDatabaseTest() {
|
|||
|
||||
val allSites = db.allSites()
|
||||
assertThat(allSites.size).isEqualTo(3)
|
||||
assertThat(allSites[0]).isEqualTo(MOCK_MODEL_1)
|
||||
assertThat(allSites[1]).isEqualTo(MOCK_MODEL_2)
|
||||
assertThat(allSites[2]).isEqualTo(MOCK_MODEL_3)
|
||||
assertThat(allSites[0]).isEqualTo(
|
||||
MOCK_MODEL_1.copy(
|
||||
headers = listOf(
|
||||
MOCK_MODEL_1.headers.first().copy(id = 1),
|
||||
MOCK_MODEL_1.headers.last().copy(id = 2)
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(allSites[1]).isEqualTo(
|
||||
MOCK_MODEL_2.copy(
|
||||
headers = listOf(
|
||||
MOCK_MODEL_2.headers.first().copy(id = 3),
|
||||
MOCK_MODEL_2.headers.last().copy(id = 4)
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(allSites[2]).isEqualTo(
|
||||
MOCK_MODEL_3.copy(
|
||||
headers = listOf(
|
||||
MOCK_MODEL_3.headers.first().copy(id = 5),
|
||||
MOCK_MODEL_3.headers.last().copy(id = 6)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun extension_put_getSite() {
|
||||
|
@ -467,10 +491,12 @@ class AppDatabaseTest() {
|
|||
)
|
||||
val updatedHeaders = listOf(
|
||||
modelToUpdate.headers.first().copy(
|
||||
id = 7,
|
||||
key = "One",
|
||||
value = "Hello"
|
||||
),
|
||||
modelToUpdate.headers.last().copy(
|
||||
id = 8,
|
||||
key = "Two",
|
||||
value = "Hey"
|
||||
)
|
||||
|
|
|
@ -35,7 +35,8 @@ fun fakeSettingsModel(
|
|||
validationMode = validationMode,
|
||||
validationArgs = null,
|
||||
disabled = false,
|
||||
networkTimeout = 10000
|
||||
networkTimeout = 10000,
|
||||
certificate = null
|
||||
)
|
||||
|
||||
fun fakeResultModel(
|
||||
|
|
|
@ -3,52 +3,56 @@ ext.versions = [
|
|||
minSdk : 21,
|
||||
compileSdk : 28,
|
||||
buildTools : '28.0.3',
|
||||
publishVersion : '0.8.5',
|
||||
publishVersionCode : 39,
|
||||
publishVersion : '0.8.8',
|
||||
publishVersionCode : 46,
|
||||
|
||||
// Plugins
|
||||
gradlePlugin : '3.2.1',
|
||||
spotlessPlugin : '3.17.0',
|
||||
versionPlugin : '0.20.0',
|
||||
gradlePlugin : '3.4.0',
|
||||
spotlessPlugin : '3.22.0',
|
||||
versionPlugin : '0.21.0',
|
||||
googleServices : '4.2.0',
|
||||
fabricPlugin : '1.+',
|
||||
|
||||
// Misc
|
||||
okHttp : '3.12.1',
|
||||
okHttp : '3.14.1',
|
||||
rhino : '1.7.10',
|
||||
|
||||
// Kotlin
|
||||
kotlin : '1.3.11',
|
||||
coroutines : '1.1.0',
|
||||
kotlin : '1.3.30',
|
||||
coroutines : '1.2.0',
|
||||
koin : '1.0.2',
|
||||
|
||||
// Google/AndroidX
|
||||
androidxAnnotations : '1.0.1',
|
||||
androidxAnnotations : '1.0.2',
|
||||
androidxCore : '1.0.2',
|
||||
androidxRecyclerView: '1.0.0',
|
||||
androidxBrowser : '1.0.0',
|
||||
googleMaterial : '1.0.0',
|
||||
room : '2.0.0',
|
||||
lifecycle : '2.0.0',
|
||||
firebaseCore : '16.0.8',
|
||||
|
||||
// Rx
|
||||
rxJava : '2.2.8',
|
||||
rxBinding : '3.0.0-alpha1',
|
||||
|
||||
// afollestad
|
||||
materialDialogs : '2.0.0-rc7',
|
||||
rxkPrefs : '1.2.1',
|
||||
materialDialogs : '2.8.1',
|
||||
rxkPrefs : '1.2.5',
|
||||
vvalidator : '0.4.1',
|
||||
|
||||
// Debugging
|
||||
timber : '4.7.1',
|
||||
fabric : '2.9.8@aar',
|
||||
fabric : '2.9.9@aar',
|
||||
|
||||
// Unit testing
|
||||
junit : '4.12',
|
||||
mockito : '2.23.4',
|
||||
mockitoKotlin : '2.0.0-RC1',
|
||||
truth : '0.42',
|
||||
mockito : '2.27.0',
|
||||
mockitoKotlin : '2.1.0',
|
||||
truth : '0.44',
|
||||
|
||||
// UI testing
|
||||
androidxTestRunner : '1.1.1',
|
||||
androidxTest : '1.1.0',
|
||||
archTesting : '2.0.0'
|
||||
archTesting : '2.0.1'
|
||||
]
|
||||
|
|
|
@ -26,6 +26,7 @@ import com.afollestad.nocknock.data.model.Status.OK
|
|||
import com.afollestad.nocknock.engine.R
|
||||
import com.afollestad.nocknock.engine.ssl.SslManager
|
||||
import com.afollestad.nocknock.engine.validation.ValidationJob.Companion.KEY_SITE_ID
|
||||
import com.afollestad.nocknock.utilities.ext.isNotNullOrEmpty
|
||||
import com.afollestad.nocknock.utilities.providers.BundleProvider
|
||||
import com.afollestad.nocknock.utilities.providers.JobInfoProvider
|
||||
import com.afollestad.nocknock.utilities.providers.StringProvider
|
||||
|
@ -35,6 +36,7 @@ import okhttp3.Response
|
|||
import org.jetbrains.annotations.TestOnly
|
||||
import java.net.SocketTimeoutException
|
||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||
import kotlin.math.max
|
||||
import timber.log.Timber.d as log
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
|
@ -153,22 +155,24 @@ class RealValidationExecutor(
|
|||
check(site.id != 0L) { "Cannot schedule validations for jobs with no ID." }
|
||||
val siteSettings = site.settings
|
||||
requireNotNull(siteSettings) { "Site settings must be populated." }
|
||||
check(siteSettings.networkTimeout > 0) { "Network timeout not set for site ${site.id}" }
|
||||
log("performValidation(${site.id}) - GET ${site.url}")
|
||||
|
||||
val request = Request.Builder()
|
||||
.apply {
|
||||
url(site.url)
|
||||
get()
|
||||
site.headers.forEach { header ->
|
||||
addHeader(header.key, header.value)
|
||||
}
|
||||
site.headers
|
||||
.filter { header -> header.key.isNotNullOrEmpty() }
|
||||
.forEach { header ->
|
||||
addHeader(header.key, header.value)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
return try {
|
||||
val clientWithTimeout = clientTimeoutChanger(okHttpClient, siteSettings.networkTimeout)
|
||||
val client = if (!siteSettings.certificate.isNullOrEmpty()) {
|
||||
val timeout = max(siteSettings.networkTimeout, 1)
|
||||
val clientWithTimeout = clientTimeoutChanger(okHttpClient, timeout)
|
||||
val client = if (siteSettings.certificate.isNotNullOrEmpty()) {
|
||||
sslManager.clientForCertificate(
|
||||
certUri = siteSettings.certificate!!,
|
||||
siteUri = site.url,
|
||||
|
@ -205,6 +209,7 @@ class RealValidationExecutor(
|
|||
)
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
ex.printStackTrace()
|
||||
log("performValidation(${site.id}) = Error: ${ex.message}")
|
||||
CheckResult(model = site.withStatus(status = ERROR, reason = ex.message))
|
||||
}
|
||||
|
|
|
@ -16,3 +16,6 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryErro
|
|||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
|
||||
|
|
BIN
ic_web.png
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 27 KiB |
42
mock/mock-google-services.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"project_info": {
|
||||
"project_number": "123456789000",
|
||||
"firebase_url": "https://mockproject-1234.firebaseio.com",
|
||||
"project_id": "mockproject-1234",
|
||||
"storage_bucket": "mockproject-1234.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:123456789000:android:f1bf012572b04063",
|
||||
"android_client_info": {
|
||||
"package_name": "com.afollestad.nocknock"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "123456789000-hjugbg6ud799v4c49dim8ce2usclthar.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzbSzCn1N6LWIe6wthYyrgUUSAlUsdqMb-wvTo"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
13
mock/mock.gradle
Normal file
|
@ -0,0 +1,13 @@
|
|||
// This script must be applied in app/build.gradle for the paths here to work correctly
|
||||
|
||||
def copyMockFilesNeeded() {
|
||||
def srcGoogleServicesFile = file("../mock/mock-google-services.json")
|
||||
def destGoogleServicesFile = file("google-services.json")
|
||||
if (!destGoogleServicesFile.exists()) {
|
||||
destGoogleServicesFile.write(srcGoogleServicesFile.text)
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
copyMockFilesNeeded()
|
||||
}
|
|
@ -18,6 +18,8 @@ dependencies {
|
|||
implementation project(':common')
|
||||
implementation project(':data')
|
||||
|
||||
api 'com.afollestad:vvalidator:' + versions.vvalidator
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:' + versions.androidxCore
|
||||
implementation 'com.google.android.material:material:' + versions.googleMaterial
|
||||
api 'androidx.lifecycle:lifecycle-extensions:' + versions.lifecycle
|
||||
|
|
|
@ -17,37 +17,24 @@ package com.afollestad.nocknock.viewcomponents.ext
|
|||
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewTreeObserver
|
||||
import androidx.annotation.DimenRes
|
||||
import com.afollestad.vvalidator.form.Condition
|
||||
|
||||
fun View.show() {
|
||||
visibility = VISIBLE
|
||||
}
|
||||
|
||||
fun View.conceal() {
|
||||
visibility = INVISIBLE
|
||||
}
|
||||
|
||||
fun View.hide() {
|
||||
visibility = GONE
|
||||
}
|
||||
|
||||
fun View.showOrHide(show: Boolean) = if (show) show() else hide()
|
||||
|
||||
fun View.onLayout(cb: () -> Unit) {
|
||||
if (this.viewTreeObserver.isAlive) {
|
||||
this.viewTreeObserver.addOnGlobalLayoutListener(
|
||||
object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
cb()
|
||||
this@onLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun View.dimenFloat(@DimenRes res: Int) = resources.getDimension(res)
|
||||
|
||||
fun View.dimenInt(@DimenRes res: Int) = resources.getDimensionPixelSize(res)
|
||||
|
||||
fun View.isVisibleCondition(): Condition = {
|
||||
visibility == VISIBLE
|
||||
}
|
||||
|
|
|
@ -56,9 +56,22 @@ class HeaderStackLayout(
|
|||
|
||||
override fun onClick(v: View) {
|
||||
val index = v.tag as Int
|
||||
list.removeViewAt(index)
|
||||
headers.removeAt(index)
|
||||
postLiveData()
|
||||
check(index >= 0 || index < list.childCount) {
|
||||
"Index $index is out of bounds in the header stack (size ${list.childCount})."
|
||||
}
|
||||
list.post {
|
||||
list.removeViewAt(index)
|
||||
headers.removeAt(index)
|
||||
invalidateTags()
|
||||
postLiveData()
|
||||
}
|
||||
}
|
||||
|
||||
private fun invalidateTags() {
|
||||
for (i in 0 until list.childCount) {
|
||||
val entry = list.getChildAt(i) as HeaderItemLayout
|
||||
entry.btnRemove.tag = i
|
||||
}
|
||||
}
|
||||
|
||||
private fun addEntry(forHeader: Header) {
|
||||
|
@ -67,9 +80,7 @@ class HeaderStackLayout(
|
|||
|
||||
val li = LayoutInflater.from(context)
|
||||
val entry = li.inflate(R.layout.header_stack_item, list, false) as HeaderItemLayout
|
||||
list.addView(entry)
|
||||
|
||||
entry.run {
|
||||
list.addView(entry.apply {
|
||||
inputKey.setText(forHeader.key)
|
||||
inputKey.post { entry.inputKey.requestFocus() }
|
||||
attachHeader(forHeader, this@HeaderStackLayout)
|
||||
|
@ -77,6 +88,6 @@ class HeaderStackLayout(
|
|||
|
||||
btnRemove.tag = headers.size - 1
|
||||
btnRemove.setOnClickListener(this@HeaderStackLayout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ import android.content.Context
|
|||
import android.util.AttributeSet
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.LinearLayout
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.afollestad.nocknock.utilities.ext.DAY
|
||||
import com.afollestad.nocknock.utilities.ext.HOUR
|
||||
|
@ -28,7 +27,7 @@ import com.afollestad.nocknock.utilities.ext.WEEK
|
|||
import com.afollestad.nocknock.viewcomponents.R
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.lifecycleOwner
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
|
||||
import com.afollestad.vvalidator.form.Form
|
||||
import kotlinx.android.synthetic.main.validation_interval_layout.view.input
|
||||
import kotlinx.android.synthetic.main.validation_interval_layout.view.spinner
|
||||
|
||||
|
@ -66,8 +65,14 @@ class ValidationIntervalLayout(
|
|||
fun attach(
|
||||
valueData: MutableLiveData<Int>,
|
||||
multiplierData: MutableLiveData<Long>,
|
||||
errorData: LiveData<Int?>
|
||||
form: Form
|
||||
) {
|
||||
form.input(input, name = "Interval") {
|
||||
isNotEmpty().description(R.string.please_enter_check_interval)
|
||||
length().greaterThan(0)
|
||||
.description(R.string.check_interval_must_be_greater_zero)
|
||||
}
|
||||
|
||||
input.attachLiveData(lifecycleOwner(), valueData)
|
||||
spinner.attachLiveData(
|
||||
lifecycleOwner = lifecycleOwner(),
|
||||
|
@ -91,10 +96,5 @@ class ValidationIntervalLayout(
|
|||
}
|
||||
}
|
||||
)
|
||||
errorData.toViewError(lifecycleOwner(), this, ::setError)
|
||||
}
|
||||
|
||||
private fun setError(error: String?) {
|
||||
input.error = error
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,15 +20,17 @@ import android.util.AttributeSet
|
|||
import android.widget.HorizontalScrollView
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.afollestad.nocknock.viewcomponents.R
|
||||
import com.afollestad.nocknock.viewcomponents.R.dimen
|
||||
import com.afollestad.nocknock.viewcomponents.R.layout
|
||||
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
|
||||
import com.afollestad.nocknock.viewcomponents.ext.dimenInt
|
||||
import com.afollestad.nocknock.viewcomponents.ext.isVisibleCondition
|
||||
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.lifecycleOwner
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||
import com.afollestad.vvalidator.form.Form
|
||||
import kotlinx.android.synthetic.main.javascript_input_layout.view.error_text
|
||||
import kotlinx.android.synthetic.main.javascript_input_layout.view.userInput
|
||||
|
||||
|
@ -50,16 +52,25 @@ class JavaScriptInputLayout(
|
|||
contentInset // bottom
|
||||
)
|
||||
elevation = dimenFloat(dimen.default_elevation)
|
||||
inflate(context, layout.javascript_input_layout, this)
|
||||
inflate(context, R.layout.javascript_input_layout, this)
|
||||
}
|
||||
|
||||
fun attach(
|
||||
codeData: MutableLiveData<String>,
|
||||
errorData: LiveData<Int?>,
|
||||
visibility: LiveData<Boolean>
|
||||
visibility: LiveData<Boolean>,
|
||||
form: Form
|
||||
) {
|
||||
form.input(userInput, name = "Script") {
|
||||
conditional(isVisibleCondition()) {
|
||||
isNotEmpty().description(R.string.please_enter_javaScript)
|
||||
}
|
||||
onErrors { _, errors ->
|
||||
val error = errors.firstOrNull()
|
||||
setError(error.toString())
|
||||
}
|
||||
}
|
||||
|
||||
userInput.attachLiveData(lifecycleOwner(), codeData)
|
||||
errorData.toViewError(lifecycleOwner(), this, ::setError)
|
||||
visibility.toViewVisibility(lifecycleOwner(), this)
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import com.afollestad.nocknock.viewcomponents.R
|
|||
import com.afollestad.nocknock.viewcomponents.ext.asSafeInt
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.lifecycleOwner
|
||||
import com.afollestad.vvalidator.form.Form
|
||||
import kotlinx.android.synthetic.main.retry_policy_layout.view.minutes
|
||||
import kotlinx.android.synthetic.main.retry_policy_layout.view.times
|
||||
import kotlinx.android.synthetic.main.retry_policy_layout.view.retry_policy_desc as description
|
||||
|
@ -41,7 +42,8 @@ class RetryPolicyLayout(
|
|||
|
||||
fun attach(
|
||||
timesData: MutableLiveData<Int>,
|
||||
minutesData: MutableLiveData<Int>
|
||||
minutesData: MutableLiveData<Int>,
|
||||
form: Form
|
||||
) {
|
||||
times.attachLiveData(lifecycleOwner(), timesData)
|
||||
minutes.attachLiveData(lifecycleOwner(), minutesData)
|
||||
|
@ -50,6 +52,13 @@ class RetryPolicyLayout(
|
|||
minutes.onTextChanged { invalidateDescriptionText() }
|
||||
|
||||
invalidateDescriptionText()
|
||||
|
||||
form.input(times, optional = true) {
|
||||
isNumber().greaterThan(0)
|
||||
}
|
||||
form.input(minutes, optional = true) {
|
||||
isNumber().greaterThan(0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun invalidateDescriptionText() {
|
||||
|
|
|
@ -23,4 +23,8 @@
|
|||
<string name="header_name">Header Name</string>
|
||||
<string name="header_value">Header Value</string>
|
||||
|
||||
<string name="please_enter_check_interval">Please input a validation interval.</string>
|
||||
<string name="check_interval_must_be_greater_zero">The validation interval must be greater than 0.</string>
|
||||
<string name="please_enter_javaScript">Please input a validation script.</string>
|
||||
|
||||
</resources>
|
||||
|
|