Compare commits

...

42 commits

Author SHA1 Message Date
Aidan Follestad
23ba4a69cd
Delete .travis.yml 2020-02-24 11:50:16 -08:00
Aidan Follestad
dd9aec1dff
Update README.md 2020-02-24 11:50:05 -08:00
Aidan Follestad
406af590aa Increment version code 2019-04-19 10:52:10 -07:00
Aidan Follestad
550f8c59be Adaptive-ish icon 2019-04-19 10:51:54 -07:00
Aidan Follestad
10d7fe33f9 Fix a few test cases 2019-04-18 19:09:08 -07:00
Aidan Follestad
35eda8f057 Exclude META-INF/atomicfu.kotlin_module from data module 2019-04-18 18:35:20 -07:00
Aidan Follestad
a0fd44ae7a Exclude META-INF/atomicfu.kotlin_module from common module 2019-04-18 18:06:27 -07:00
Aidan Follestad
351f718df8 0.8.8 2019-04-18 17:57:20 -07:00
Aidan Follestad
e2f7db22d1 Still trying to fix Travis 2019-04-18 17:48:27 -07:00
Aidan Follestad
82c1a17c68 Fix crash when mremoving headers, resolves #48 2019-04-18 17:36:49 -07:00
Aidan Follestad
a6670e2bea Fix Travis build 2019-04-18 17:20:09 -07:00
Aidan Follestad
5fc1569099 Remove donation options 2019-04-18 16:19:10 -07:00
Aidan Follestad
0770db5df5 Attempt to fix Travis.ci running UI tests 2019-04-18 16:12:06 -07:00
Aidan Follestad
97a0eda92c Dep and Gradle Plugin updates 2019-04-18 16:07:24 -07:00
Aidan Follestad
1ccb89bfc3 Dependency upgrades 2019-04-15 20:46:36 -07:00
Aidan Follestad
9ea9c78099 Add privacy policy link to the about dialog 2019-04-15 20:37:35 -07:00
Aidan Follestad
997c797598
Kotlin 1.3.30 2019-04-11 12:56:23 -07:00
Aidan Follestad
b26543d244
Update dependencies.gradle 2019-03-16 12:08:29 -07:00
Aidan Follestad
8c3654c4ac 0.8.7 2019-03-14 15:06:38 -07:00
Aidan Follestad
df2652860e -am 2019-03-14 15:06:03 -07:00
Aidan Follestad
4da8cb5f11 Fix tests 2019-03-14 15:01:24 -07:00
Aidan Follestad
334e9e823c Switch to Firebase for Crashlytics 2019-03-14 13:55:19 -07:00
Aidan Follestad
6d382b93a5 Dependency updates 2019-02-21 14:43:26 -08:00
Aidan Follestad
ef18464728
vvalidator 0.3.0 2019-02-01 17:57:23 -08:00
Aidan Follestad
872e99d80d Custom repo not needed anymore for Material Dialogs 2019-02-01 15:21:11 -08:00
Aidan Follestad
7f507792a8
Update gradle-wrapper.properties 2019-01-30 13:53:10 -08:00
Aidan Follestad
68b6944542 0.8.6c 2019-01-26 18:42:16 -08:00
Aidan Follestad
e39093b526 Fix script input layout background color 2019-01-26 18:41:55 -08:00
Aidan Follestad
9514a5ec83 0.8.6b 2019-01-26 14:55:26 -08:00
Aidan Follestad
3e5b1d4d8e Resolve two crashes 2019-01-26 14:55:06 -08:00
Aidan Follestad
de59bf9ec1 0.8.6 2019-01-26 14:41:02 -08:00
Aidan Follestad
0fbd27b54b Fixed some data population issues 2019-01-26 14:35:34 -08:00
Aidan Follestad
33388bd5c2 Fixes around default SSL cert setting 2019-01-26 14:27:55 -08:00
Aidan Follestad
75297c7ff5 Fixes to vvalidator setup 2019-01-26 14:22:22 -08:00
Aidan Follestad
c6fca52fe4 Use vvalidator in the view site page 2019-01-26 14:10:58 -08:00
Aidan Follestad
b3f8a43f71 Use vvalidator in the add site page 2019-01-26 13:55:34 -08:00
Aidan Follestad
7dc4ee7fb1 Add vvalidator dep 2019-01-26 12:39:46 -08:00
Aidan Follestad
859dcb53ca MD rc9 2019-01-26 12:36:20 -08:00
Aidan Follestad
f86ccbbe0c Add default Fabric props that should fix CI 2019-01-25 11:08:18 -08:00
Aidan Follestad
571e7ebff3 Pull Fabric props from environment variables, default to 0 which stops build failures 2019-01-24 16:03:12 -08:00
Aidan Follestad
77f939b095 Lots of dependency upgrades 2019-01-24 14:32:38 -08:00
Aidan Follestad
8f16ff2d33 AppDatabaseTest fix 2019-01-13 15:16:33 -08:00
65 changed files with 491 additions and 999 deletions

View file

@ -1,3 +0,0 @@
[*.kt]
indent_size = 2
continuation_indent_size=4

4
.gitignore vendored
View file

@ -180,4 +180,6 @@ gradle-app.setting
.gradletasknamecache .gradletasknamecache
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 # # 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
View file

@ -40,7 +40,7 @@
</value> </value>
</option> </option>
</component> </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" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View file

@ -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

View file

@ -1,6 +1,5 @@
## Nock Nock ## Nock Nock
[![Build Status](https://travis-ci.org/afollestad/nock-nock.svg)](https://travis-ci.org/afollestad/nock-nock)
[![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg?style=flat-square)](https://www.apache.org/licenses/LICENSE-2.0.html) [![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg?style=flat-square)](https://www.apache.org/licenses/LICENSE-2.0.html)
![Showcase](https://raw.githubusercontent.com/afollestad/nock-nock/master/art/showcase5.png) ![Showcase](https://raw.githubusercontent.com/afollestad/nock-nock/master/art/showcase5.png)

View file

@ -4,18 +4,6 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions' 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 { android {
compileSdkVersion versions.compileSdk compileSdkVersion versions.compileSdk
buildToolsVersion versions.buildTools buildToolsVersion versions.buildTools
@ -26,17 +14,15 @@ android {
targetSdkVersion versions.compileSdk targetSdkVersion versions.compileSdk
versionCode versions.publishVersionCode versionCode versions.publishVersionCode
versionName versions.publishVersion versionName versions.publishVersion
manifestPlaceholders = [fabricKey:getFabricApiKey()]
} }
buildTypes { compileOptions {
debug { sourceCompatibility 1.8
ext.enableCrashlytics = false targetCompatibility 1.8
buildConfigField "String", "FABRIC_API_KEY", "\"\"" }
}
release { packagingOptions {
buildConfigField "String", "FABRIC_API_KEY", "\"${getFabricApiKey()}\"" exclude 'META-INF/atomicfu.kotlin_module'
}
} }
} }
@ -52,6 +38,7 @@ dependencies {
implementation 'androidx.recyclerview:recyclerview:' + versions.androidxRecyclerView implementation 'androidx.recyclerview:recyclerview:' + versions.androidxRecyclerView
implementation 'com.google.android.material:material:' + versions.googleMaterial implementation 'com.google.android.material:material:' + versions.googleMaterial
implementation 'androidx.browser:browser:' + versions.androidxBrowser implementation 'androidx.browser:browser:' + versions.androidxBrowser
implementation 'com.google.firebase:firebase-core:' + versions.firebaseCore
// Lifecycle // Lifecycle
kapt 'androidx.lifecycle:lifecycle-compiler:' + versions.lifecycle kapt 'androidx.lifecycle:lifecycle-compiler:' + versions.lifecycle
@ -85,4 +72,8 @@ dependencies {
androidTestImplementation 'androidx.test:rules:' + versions.androidxTestRunner 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'

View file

@ -47,10 +47,8 @@ class NockNockApp : Application() {
Timber.plant(DebugTree()) Timber.plant(DebugTree())
} }
if (BuildConfig.FABRIC_API_KEY.isNotEmpty()) { Timber.plant(FabricTree())
Timber.plant(FabricTree()) Fabric.with(this, Crashlytics())
Fabric.with(this, Crashlytics())
}
val modules = listOf( val modules = listOf(
prefModule, prefModule,

View file

@ -15,10 +15,15 @@
*/ */
package com.afollestad.nocknock.ui package com.afollestad.nocknock.ui
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.afollestad.nocknock.R import com.afollestad.nocknock.R
import com.afollestad.nocknock.koin.PREF_DARK_MODE 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.nocknock.utilities.rx.attachLifecycle
import com.afollestad.rxkprefs.Pref import com.afollestad.rxkprefs.Pref
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
@ -35,16 +40,35 @@ abstract class DarkModeSwitchActivity : AppCompatActivity() {
setTheme(themeRes()) setTheme(themeRes())
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
darkModePref.observe() if (getCurrentNightMode() == UNKNOWN) {
.filter { it != isDarkModeEnabled } darkModePref.observe()
.subscribe { .filter { it != isDarkModeEnabled }
log("Theme changed, recreating Activity.") .subscribe {
recreate() log("Theme changed, recreating Activity.")
} recreate()
.attachLifecycle(this) }
.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()) protected fun toggleDarkMode() = setDarkMode(!isDarkMode())

View 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
}

View file

@ -31,11 +31,13 @@ import com.afollestad.nocknock.utilities.ext.onTextChanged
import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
import com.afollestad.nocknock.utilities.livedata.distinct import com.afollestad.nocknock.utilities.livedata.distinct
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat 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.ext.onScroll
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData 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.toViewText
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility 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.checkIntervalLayout
import kotlinx.android.synthetic.main.activity_addsite.headersLayout import kotlinx.android.synthetic.main.activity_addsite.headersLayout
import kotlinx.android.synthetic.main.activity_addsite.inputName import kotlinx.android.synthetic.main.activity_addsite.inputName
@ -64,12 +66,14 @@ class AddSiteActivity : DarkModeSwitchActivity() {
} }
private val viewModel by viewModel<AddSiteViewModel>() private val viewModel by viewModel<AddSiteViewModel>()
private lateinit var validationForm: Form
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_addsite) setContentView(R.layout.activity_addsite)
setupUi() setupUi()
setupValidation()
lifecycle.addObserver(viewModel) lifecycle.addObserver(viewModel)
@ -82,23 +86,17 @@ class AddSiteActivity : DarkModeSwitchActivity() {
// Name // Name
inputName.attachLiveData(this, viewModel.name) inputName.attachLiveData(this, viewModel.name)
viewModel.onNameError()
.toViewError(this, inputName)
// Tags // Tags
inputTags.attachLiveData(this, viewModel.tags) inputTags.attachLiveData(this, viewModel.tags)
// Url // Url
inputUrl.attachLiveData(this, viewModel.url) inputUrl.attachLiveData(this, viewModel.url)
viewModel.onUrlError()
.toViewError(this, inputUrl)
viewModel.onUrlWarningVisibility() viewModel.onUrlWarningVisibility()
.toViewVisibility(this, textUrlWarning) .toViewVisibility(this, textUrlWarning)
// Timeout // Timeout
responseTimeoutInput.attachLiveData(this, viewModel.timeout) responseTimeoutInput.attachLiveData(this, viewModel.timeout)
viewModel.onTimeoutError()
.toViewError(this, responseTimeoutInput)
// Validation mode // Validation mode
responseValidationMode.attachLiveData( responseValidationMode.attachLiveData(
@ -107,8 +105,6 @@ class AddSiteActivity : DarkModeSwitchActivity() {
outTransformer = { ValidationMode.fromIndex(it) }, outTransformer = { ValidationMode.fromIndex(it) },
inTransformer = { it.toIndex() } inTransformer = { it.toIndex() }
) )
viewModel.onValidationSearchTermError()
.toViewError(this, responseValidationSearchTerm)
viewModel.onValidationModeDescription() viewModel.onValidationModeDescription()
.toViewText(this, validationModeDescription) .toViewText(this, validationModeDescription)
@ -121,32 +117,10 @@ class AddSiteActivity : DarkModeSwitchActivity() {
viewModel.onValidationSearchTermVisibility() viewModel.onValidationSearchTermVisibility()
.toViewVisibility(this, responseValidationSearchTerm) .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 // SSL certificate
sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it } sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it }
viewModel.certificateUri.distinct() viewModel.certificateUri.distinct()
.observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) }) .observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) })
viewModel.onCertificateError()
.toViewError(this, sslCertificateInput)
// Headers // Headers
headersLayout.attach(viewModel.headers) headersLayout.attach(viewModel.headers)
@ -156,15 +130,6 @@ class AddSiteActivity : DarkModeSwitchActivity() {
toolbarTitle.setText(R.string.add_site) toolbarTitle.setText(R.string.add_site)
toolbar.run { toolbar.run {
inflateMenu(R.menu.menu_addsite) 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) setNavigationIcon(R.drawable.ic_action_close)
setNavigationOnClickListener { finish() } 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() { override fun onResume() {
super.onResume() super.onResume()
appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) { appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {

View file

@ -39,9 +39,7 @@ import com.afollestad.nocknock.data.putSite
import com.afollestad.nocknock.engine.validation.ValidationExecutor import com.afollestad.nocknock.engine.validation.ValidationExecutor
import com.afollestad.nocknock.ui.ScopedViewModel import com.afollestad.nocknock.ui.ScopedViewModel
import com.afollestad.nocknock.utilities.ext.MINUTE 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.utilities.livedata.map
import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -83,23 +81,10 @@ class AddSiteViewModel(
headers.value = emptyList() headers.value = emptyList()
} }
// Private properties
private val isLoading = MutableLiveData<Boolean>() 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 onIsLoading(): LiveData<Boolean> = isLoading
@CheckResult fun onNameError(): LiveData<Int?> = nameError
@CheckResult fun onUrlError(): LiveData<Int?> = urlError
@CheckResult fun onUrlWarningVisibility(): LiveData<Boolean> { @CheckResult fun onUrlWarningVisibility(): LiveData<Boolean> {
return url.map { return url.map {
val parsed = HttpUrl.parse(it) val parsed = HttpUrl.parse(it)
@ -107,8 +92,6 @@ class AddSiteViewModel(
} }
} }
@CheckResult fun onTimeoutError(): LiveData<Int?> = timeoutError
@CheckResult fun onValidationModeDescription(): LiveData<Int> { @CheckResult fun onValidationModeDescription(): LiveData<Int> {
return validationMode.map { return validationMode.map {
when (it!!) { 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() = @CheckResult fun onValidationScriptVisibility() = validationMode.map { it == JAVASCRIPT }
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
// Actions // Actions
fun commit(done: () -> Unit) { fun commit(done: () -> Unit) {
@ -171,89 +144,7 @@ class AddSiteViewModel(
} }
private fun generateDbModel(): Site? { 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 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 { it.trim() } ?: ""
val newSettings = SiteSettings( val newSettings = SiteSettings(
@ -262,7 +153,7 @@ class AddSiteViewModel(
validationArgs = getValidationArgs(), validationArgs = getValidationArgs(),
networkTimeout = timeout, networkTimeout = timeout,
disabled = false, disabled = false,
certificate = certificateUri.value.toString() certificate = certificateUri.value?.toString()
) )
val newLastResult = ValidationResult( val newLastResult = ValidationResult(
@ -275,7 +166,8 @@ class AddSiteViewModel(
val retryPolicyMinutes = retryPolicyMinutes.value ?: 0 val retryPolicyMinutes = retryPolicyMinutes.value ?: 0
val newRetryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) { val newRetryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) {
RetryPolicy( RetryPolicy(
count = retryPolicyTimes, minutes = retryPolicyMinutes count = retryPolicyTimes,
minutes = retryPolicyMinutes
) )
} else { } else {
null null

View file

@ -24,7 +24,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems import com.afollestad.materialdialogs.list.listItems
import com.afollestad.materialdialogs.list.listItemsSingleChoice
import com.afollestad.nocknock.R import com.afollestad.nocknock.R
import com.afollestad.nocknock.adapter.SiteAdapter import com.afollestad.nocknock.adapter.SiteAdapter
import com.afollestad.nocknock.adapter.TagAdapter 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.dialogs.AboutDialog
import com.afollestad.nocknock.notifications.NockNotificationManager import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.ui.DarkModeSwitchActivity 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.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 com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
import kotlinx.android.synthetic.main.activity_main.fab import kotlinx.android.synthetic.main.activity_main.fab
import kotlinx.android.synthetic.main.activity_main.list import kotlinx.android.synthetic.main.activity_main.list
@ -93,12 +90,17 @@ class MainActivity : DarkModeSwitchActivity() {
toolbar.run { toolbar.run {
inflateMenu(R.menu.menu_main) inflateMenu(R.menu.menu_main)
menu.findItem(R.id.dark_mode) menu.findItem(R.id.dark_mode)
.isChecked = isDarkMode() .apply {
if (getCurrentNightMode() == UNKNOWN) {
isChecked = isDarkMode()
} else {
isVisible = false
}
}
setOnMenuItemClickListener { item -> setOnMenuItemClickListener { item ->
when (item.itemId) { when (item.itemId) {
R.id.about -> AboutDialog.show(this@MainActivity) R.id.about -> AboutDialog.show(this@MainActivity)
R.id.dark_mode -> toggleDarkMode() R.id.dark_mode -> toggleDarkMode()
R.id.support_me -> supportMe()
} }
return@setOnMenuItemClickListener true return@setOnMenuItemClickListener true
} }
@ -144,20 +146,4 @@ class MainActivity : DarkModeSwitchActivity() {
viewSite(model) 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)
}
}
} }

View file

@ -32,11 +32,13 @@ import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
import com.afollestad.nocknock.utilities.livedata.distinct import com.afollestad.nocknock.utilities.livedata.distinct
import com.afollestad.nocknock.utilities.providers.IntentProvider import com.afollestad.nocknock.utilities.providers.IntentProvider
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat 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.ext.onScroll
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData 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.toViewText
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility 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.checkIntervalLayout
import kotlinx.android.synthetic.main.activity_viewsite.headersLayout import kotlinx.android.synthetic.main.activity_viewsite.headersLayout
import kotlinx.android.synthetic.main.activity_viewsite.iconStatus import kotlinx.android.synthetic.main.activity_viewsite.iconStatus
@ -69,6 +71,7 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
} }
internal val viewModel by viewModel<ViewSiteViewModel>() internal val viewModel by viewModel<ViewSiteViewModel>()
private lateinit var validationForm: Form
private val intentProvider by inject<IntentProvider>() private val intentProvider by inject<IntentProvider>()
private val statusUpdateReceiver by lazy { private val statusUpdateReceiver by lazy {
@ -81,17 +84,18 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_viewsite) setContentView(R.layout.activity_viewsite)
setupUi()
lifecycle.run {
addObserver(viewModel)
addObserver(statusUpdateReceiver)
}
// Populate view model with initial data // Populate view model with initial data
val model = intent.getSerializableExtra(KEY_SITE) as Site val model = intent.getSerializableExtra(KEY_SITE) as Site
viewModel.setModel(model) viewModel.setModel(model)
setupUi()
setupValidation()
lifecycle.run {
addObserver(viewModel)
addObserver(statusUpdateReceiver)
}
// Loading // Loading
loadingProgress.observe(this, viewModel.onIsLoading()) loadingProgress.observe(this, viewModel.onIsLoading())
@ -103,23 +107,17 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
// Name // Name
inputName.attachLiveData(this, viewModel.name) inputName.attachLiveData(this, viewModel.name)
viewModel.onNameError()
.toViewError(this, inputName)
// Tags // Tags
inputTags.attachLiveData(this, viewModel.tags) inputTags.attachLiveData(this, viewModel.tags)
// Url // Url
inputUrl.attachLiveData(this, viewModel.url) inputUrl.attachLiveData(this, viewModel.url)
viewModel.onUrlError()
.toViewError(this, inputUrl)
viewModel.onUrlWarningVisibility() viewModel.onUrlWarningVisibility()
.toViewVisibility(this, textUrlWarning) .toViewVisibility(this, textUrlWarning)
// Timeout // Timeout
responseTimeoutInput.attachLiveData(this, viewModel.timeout) responseTimeoutInput.attachLiveData(this, viewModel.timeout)
viewModel.onTimeoutError()
.toViewError(this, responseTimeoutInput)
// Validation mode // Validation mode
responseValidationMode.attachLiveData( responseValidationMode.attachLiveData(
@ -128,8 +126,6 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
outTransformer = { ValidationMode.fromIndex(it) }, outTransformer = { ValidationMode.fromIndex(it) },
inTransformer = { it.toIndex() } inTransformer = { it.toIndex() }
) )
viewModel.onValidationSearchTermError()
.toViewError(this, responseValidationSearchTerm)
viewModel.onValidationModeDescription() viewModel.onValidationModeDescription()
.toViewText(this, validationModeDescription) .toViewText(this, validationModeDescription)
@ -138,32 +134,10 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
viewModel.onValidationSearchTermVisibility() viewModel.onValidationSearchTermVisibility()
.toViewVisibility(this, responseValidationSearchTerm) .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 // SSL certificate
sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it } sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it }
viewModel.certificateUri.distinct() viewModel.certificateUri.distinct()
.observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) }) .observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) })
viewModel.onCertificateError()
.toViewError(this, sslCertificateInput)
// Headers // Headers
headersLayout.attach(viewModel.headers) headersLayout.attach(viewModel.headers)
@ -190,7 +164,6 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
setOnMenuItemClickListener { setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.commit -> viewModel.commit { finish() }
R.id.remove -> maybeRemoveSite() R.id.remove -> maybeRemoveSite()
R.id.disableChecks -> maybeDisableChecks() 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() { override fun onResume() {
super.onResume() super.onResume()
appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) { appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {

View file

@ -40,11 +40,9 @@ import com.afollestad.nocknock.engine.validation.ValidationExecutor
import com.afollestad.nocknock.notifications.NockNotificationManager import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.ui.ScopedViewModel import com.afollestad.nocknock.ui.ScopedViewModel
import com.afollestad.nocknock.utilities.ext.formatDate 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.map
import com.afollestad.nocknock.utilities.livedata.zip import com.afollestad.nocknock.utilities.livedata.zip
import com.afollestad.nocknock.utilities.providers.StringProvider import com.afollestad.nocknock.utilities.providers.StringProvider
import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -81,23 +79,10 @@ class ViewSiteViewModel(
internal val disabled = MutableLiveData<Boolean>() internal val disabled = MutableLiveData<Boolean>()
internal val lastResult = MutableLiveData<ValidationResult?>() internal val lastResult = MutableLiveData<ValidationResult?>()
// Private properties
private val isLoading = MutableLiveData<Boolean>() 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 onIsLoading(): LiveData<Boolean> = isLoading
@CheckResult fun onNameError(): LiveData<Int?> = nameError
@CheckResult fun onUrlError(): LiveData<Int?> = urlError
@CheckResult fun onUrlWarningVisibility(): LiveData<Boolean> { @CheckResult fun onUrlWarningVisibility(): LiveData<Boolean> {
return url.map { return url.map {
val parsed = HttpUrl.parse(it) val parsed = HttpUrl.parse(it)
@ -105,8 +90,6 @@ class ViewSiteViewModel(
} }
} }
@CheckResult fun onTimeoutError(): LiveData<Int?> = timeoutError
@CheckResult fun onValidationModeDescription(): LiveData<Int> { @CheckResult fun onValidationModeDescription(): LiveData<Int> {
return validationMode.map { return validationMode.map {
when (it!!) { 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() = @CheckResult fun onValidationScriptVisibility() = validationMode.map { it == JAVASCRIPT }
validationMode.map { it == TERM_SEARCH }
@CheckResult fun onValidationScriptError(): LiveData<Int?> = validationScriptError @CheckResult fun onDisableChecksVisibility(): LiveData<Boolean> = disabled.map { !it }
@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 onDoneButtonText(): LiveData<Int> = @CheckResult fun onDoneButtonText(): LiveData<Int> =
disabled.map { disabled.map {
@ -250,90 +222,8 @@ class ViewSiteViewModel(
} }
private fun getUpdatedDbModel(): Site? { 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 val timeout = timeout.value ?: 10_000
if (timeout < 0) { val cleanedTags = tags.value?.split(',')?.joinToString(separator = ",") ?: ""
timeoutError.value = R.string.please_enter_networkTimeout
errorCount++
} else {
timeoutError.value = null
}
// Validate check interval
if (checkIntervalValue.value.isNullOrLessThan(1)) {
checkIntervalValueError.value = R.string.please_enter_check_interval
errorCount++
} else {
checkIntervalValueError.value = null
}
// Validate arguments
if (validationMode.value == TERM_SEARCH &&
validationSearchTerm.value.isNullOrEmpty()
) {
errorCount++
validationSearchTermError.value = R.string.please_enter_search_term
validationScriptError.value = null
} else if (validationMode.value == JAVASCRIPT &&
validationScript.value.isNullOrEmpty()
) {
errorCount++
validationSearchTermError.value = null
validationScriptError.value = R.string.please_enter_javaScript
} else {
validationSearchTermError.value = null
validationScriptError.value = null
}
// Validate SSL certificate
val certString = certificateUri.value
if (certString != null) {
val rawCertUri = certString.toUri()
val certUri = if (rawCertUri.scheme == null) {
rawCertUri.buildUpon()
.scheme("file")
.build()
} else {
rawCertUri
}
if (certUri.scheme != "content" && certUri.scheme != "file") {
errorCount++
certificateError.value = R.string.please_enter_validCertUri
} else {
certificateError.value = null
}
}
if (errorCount > 0) {
return null
}
val cleanedTags = tags.value?.split(',')?.joinToString { it.trim() } ?: ""
val newSettings = site.settings!!.copy( val newSettings = site.settings!!.copy(
validationIntervalMs = getCheckIntervalMs(), validationIntervalMs = getCheckIntervalMs(),
@ -341,7 +231,7 @@ class ViewSiteViewModel(
validationArgs = getValidationArgs(), validationArgs = getValidationArgs(),
networkTimeout = timeout, networkTimeout = timeout,
disabled = false, disabled = false,
certificate = certificateUri.value.toString() certificate = certificateUri.value?.toString()
) )
val retryPolicyTimes = retryPolicyTimes.value ?: 0 val retryPolicyTimes = retryPolicyTimes.value ?: 0
@ -349,11 +239,15 @@ class ViewSiteViewModel(
val retryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) { val retryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) {
if (site.retryPolicy != null) { if (site.retryPolicy != null) {
// Have existing policy, update it // Have existing policy, update it
site.retryPolicy!!.copy(count = retryPolicyTimes, minutes = retryPolicyMinutes) site.retryPolicy!!.copy(
count = retryPolicyTimes,
minutes = retryPolicyMinutes
)
} else { } else {
// Create new policy // Create new policy
RetryPolicy( RetryPolicy(
count = retryPolicyTimes, minutes = retryPolicyMinutes count = retryPolicyTimes,
minutes = retryPolicyMinutes
) )
} }
} else { } else {

View file

@ -55,7 +55,11 @@ fun ViewSiteViewModel.setModel(site: Site) {
setCheckInterval(settings.validationIntervalMs) setCheckInterval(settings.validationIntervalMs)
setRetryPolicy(site.retryPolicy) setRetryPolicy(site.retryPolicy)
headers.value = site.headers 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.disabled.value = settings.disabled
this.lastResult.value = site.lastResult this.lastResult.value = site.lastResult
@ -65,22 +69,22 @@ private fun ViewSiteViewModel.setCheckInterval(interval: Long) {
when { when {
interval >= WEEK -> { interval >= WEEK -> {
checkIntervalValue.value = checkIntervalValue.value =
getIntervalFromUnit(interval, WEEK) getIntervalFromUnit(interval, WEEK)
checkIntervalUnit.value = WEEK checkIntervalUnit.value = WEEK
} }
interval >= DAY -> { interval >= DAY -> {
checkIntervalValue.value = checkIntervalValue.value =
getIntervalFromUnit(interval, DAY) getIntervalFromUnit(interval, DAY)
checkIntervalUnit.value = DAY checkIntervalUnit.value = DAY
} }
interval >= HOUR -> { interval >= HOUR -> {
checkIntervalValue.value = checkIntervalValue.value =
getIntervalFromUnit(interval, HOUR) getIntervalFromUnit(interval, HOUR)
checkIntervalUnit.value = HOUR checkIntervalUnit.value = HOUR
} }
interval >= MINUTE -> { interval >= MINUTE -> {
checkIntervalValue.value = checkIntervalValue.value =
getIntervalFromUnit(interval, MINUTE) getIntervalFromUnit(interval, MINUTE)
checkIntervalUnit.value = MINUTE checkIntervalUnit.value = MINUTE
} }
else -> { else -> {

View file

@ -123,7 +123,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/content_inset" android:layout_marginBottom="@dimen/content_inset"
android:layout_marginTop="@dimen/content_inset_half" android:layout_marginTop="@dimen/content_inset_half"
android:background="@color/lighterGray" android:background="?scriptLayoutBackground"
/> />
<TextView <TextView

View file

@ -161,7 +161,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/content_inset" android:layout_marginBottom="@dimen/content_inset"
android:layout_marginTop="@dimen/content_inset_half" android:layout_marginTop="@dimen/content_inset_half"
android:background="@color/lighterGray" android:background="?scriptLayoutBackground"
/> />
<TextView <TextView

View file

@ -7,7 +7,4 @@
android:id="@+id/dark_mode" android:id="@+id/dark_mode"
android:checkable="true" android:checkable="true"
android:title="@string/dark_mode"/> android:title="@string/dark_mode"/>
<item
android:id="@+id/support_me"
android:title="@string/support_me"/>
</menu> </menu>

View 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>

View 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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -13,10 +13,4 @@
<item>JavaScript Evaluation</item> <item>JavaScript Evaluation</item>
</string-array> </string-array>
<string-array name="donation_options">
<item>via PayPal</item>
<item>via Cash App</item>
<item>via Venmo</item>
</string-array>
</resources> </resources>

View file

@ -4,5 +4,6 @@
<attr format="color" name="toolbarTitleColor"/> <attr format="color" name="toolbarTitleColor"/>
<attr format="color" name="dividerColor"/> <attr format="color" name="dividerColor"/>
<attr format="color" name="iconColor"/> <attr format="color" name="iconColor"/>
<attr format="color" name="scriptLayoutBackground"/>
</resources> </resources>

View file

@ -7,7 +7,9 @@
<color name="colorPrimary_darkTheme">#212121</color> <color name="colorPrimary_darkTheme">#212121</color>
<color name="colorPrimaryDark_darkTheme">#252525</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">#FF6E40</color>
<color name="colorAccent_pressed">#E44615</color> <color name="colorAccent_pressed">#E44615</color>
<color name="colorAccent_translucent">#40FF6E40</color> <color name="colorAccent_translucent">#40FF6E40</color>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#758F9A</color>
</resources>

View file

@ -14,6 +14,7 @@
<a href=\'https://www.linkedin.com/in/afollestad\'>LinkedIn</a> <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/><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/>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>
<string name="dark_mode">Dark Mode</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_name">Please enter a name!</string>
<string name="please_enter_url">Please enter a URL.</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_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_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_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> <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. exception to pass custom error messages to Nock Nock.
</string> </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> <string name="install_web_browser">Please install a web browser app, such as Google Chrome.</string>
</resources> </resources>

View file

@ -6,7 +6,7 @@
<style name="PrimaryDarkButton" parent="Widget.MaterialComponents.Button"> <style name="PrimaryDarkButton" parent="Widget.MaterialComponents.Button">
<item name="android:textColor">#fff</item> <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> <item name="android:fontFamily">@font/lato</item>
</style> </style>

View file

@ -9,6 +9,7 @@
<item name="toolbarTitleColor">#000000</item> <item name="toolbarTitleColor">#000000</item>
<item name="dividerColor">#EEEEEE</item> <item name="dividerColor">#EEEEEE</item>
<item name="iconColor">#000000</item> <item name="iconColor">#000000</item>
<item name="scriptLayoutBackground">@color/lighterGray</item>
<item name="android:textColorPrimary">#212121</item> <item name="android:textColorPrimary">#212121</item>
<item name="android:textColorSecondary">#727272</item> <item name="android:textColorSecondary">#727272</item>
@ -33,6 +34,7 @@
<item name="toolbarTitleColor">#ffffff</item> <item name="toolbarTitleColor">#ffffff</item>
<item name="dividerColor">#303030</item> <item name="dividerColor">#303030</item>
<item name="iconColor">#FFFFFF</item> <item name="iconColor">#FFFFFF</item>
<item name="scriptLayoutBackground">@color/darkerGray</item>
<item name="android:textColorPrimary">#FFFFFF</item> <item name="android:textColorPrimary">#FFFFFF</item>
<item name="android:textColorSecondary">#F0F0F0</item> <item name="android:textColorSecondary">#F0F0F0</item>

View file

@ -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>

View file

@ -17,20 +17,21 @@ package com.afollestad.nocknock.ui.addsite
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.afollestad.nocknock.R 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.Site
import com.afollestad.nocknock.data.model.SiteSettings 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.JAVASCRIPT
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH 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.engine.validation.ValidationExecutor
import com.afollestad.nocknock.mockDatabase import com.afollestad.nocknock.mockDatabase
import com.afollestad.nocknock.utilities.ext.MINUTE import com.afollestad.nocknock.utilities.ext.MINUTE
import com.afollestad.nocknock.utilities.livedata.test import com.afollestad.nocknock.utilities.livedata.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.argumentCaptor import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verify
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -149,247 +150,9 @@ class AddSiteViewModelTest {
assertThat(viewModel.getValidationArgs()).isEqualTo("Two") 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 { @Test fun commit_success() = runBlocking {
val isLoading = viewModel.onIsLoading() val isLoading = viewModel.onIsLoading()
.test() .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() fillInModel()
val onDone = mock<() -> Unit>() val onDone = mock<() -> Unit>()
@ -397,31 +160,30 @@ class AddSiteViewModelTest {
val siteCaptor = argumentCaptor<Site>() val siteCaptor = argumentCaptor<Site>()
val settingsCaptor = argumentCaptor<SiteSettings>() val settingsCaptor = argumentCaptor<SiteSettings>()
val validationResultCaptor = argumentCaptor<ValidationResult>()
isLoading.assertValues(true, false) isLoading.assertValues(true, false)
verify(database.siteDao()).insert(siteCaptor.capture()) verify(database.siteDao()).insert(siteCaptor.capture())
verify(database.siteSettingsDao()).insert(settingsCaptor.capture()) verify(database.siteSettingsDao()).insert(settingsCaptor.capture())
verify(database.validationResultsDao(), never()).insert(any()) verify(database.validationResultsDao()).insert(validationResultCaptor.capture())
val settings = settingsCaptor.firstValue val settings = settingsCaptor.firstValue
val result = validationResultCaptor.firstValue.copy(siteId = 1)
val model = siteCaptor.firstValue.copy( val model = siteCaptor.firstValue.copy(
id = 1, // fill it in because our insert captor doesn't catch this id = 1, // fill it in because our insert captor doesn't catch this
settings = settings, settings = settings,
lastResult = null lastResult = result
) )
assertThat(result.reason).isNull()
assertThat(result.status).isEqualTo(WAITING)
verify(validationManager).scheduleValidation( verify(validationManager).scheduleValidation(
site = model, site = model,
rightNow = true, rightNow = true,
cancelPrevious = true, cancelPrevious = true,
fromFinishingJob = false fromFinishingJob = false
) )
onNameError.assertNoValues()
onUrlError.assertNoValues()
onTimeoutError.assertNoValues()
onCheckIntervalError.assertNoValues()
onSearchTermError.assertNoValues()
onScriptError.assertNoValues()
verify(onDone).invoke() verify(onDone).invoke()
} }
@ -435,5 +197,10 @@ class AddSiteViewModelTest {
validationScript.value = null validationScript.value = null
checkIntervalValue.value = 60 checkIntervalValue.value = 60
checkIntervalUnit.value = 1000 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")
)
} }
} }

View file

@ -18,6 +18,8 @@ package com.afollestad.nocknock.ui.viewsite
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.afollestad.nocknock.MOCK_MODEL_1 import com.afollestad.nocknock.MOCK_MODEL_1
import com.afollestad.nocknock.R 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.Site
import com.afollestad.nocknock.data.model.SiteSettings import com.afollestad.nocknock.data.model.SiteSettings
import com.afollestad.nocknock.data.model.Status.CHECKING 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.ValidationMode.TERM_SEARCH
import com.afollestad.nocknock.data.model.ValidationResult import com.afollestad.nocknock.data.model.ValidationResult
import com.afollestad.nocknock.engine.validation.ValidationExecutor import com.afollestad.nocknock.engine.validation.ValidationExecutor
import com.afollestad.nocknock.fakeRetryPolicy
import com.afollestad.nocknock.mockDatabase import com.afollestad.nocknock.mockDatabase
import com.afollestad.nocknock.notifications.NockNotificationManager import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.utilities.livedata.test 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.any
import com.nhaarman.mockitokotlin2.argumentCaptor import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.doAnswer import com.nhaarman.mockitokotlin2.doAnswer
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -255,247 +259,11 @@ class ViewSiteViewModelTest {
.isEqualTo("Two") .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 { @Test fun commit_success() = runBlocking {
whenever(database.retryPolicyDao().forSite(any())).doReturn(listOf(fakeRetryPolicy(1)))
val isLoading = viewModel.onIsLoading() val isLoading = viewModel.onIsLoading()
.test() .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() fillInModel()
val onDone = mock<() -> Unit>() val onDone = mock<() -> Unit>()
@ -506,11 +274,13 @@ class ViewSiteViewModelTest {
val siteCaptor = argumentCaptor<Site>() val siteCaptor = argumentCaptor<Site>()
val settingsCaptor = argumentCaptor<SiteSettings>() val settingsCaptor = argumentCaptor<SiteSettings>()
val resultCaptor = argumentCaptor<ValidationResult>() val resultCaptor = argumentCaptor<ValidationResult>()
val retryPolicyCaptor = argumentCaptor<RetryPolicy>()
isLoading.assertValues(true, false) isLoading.assertValues(true, false)
verify(database.siteDao()).update(siteCaptor.capture()) verify(database.siteDao()).update(siteCaptor.capture())
verify(database.siteSettingsDao()).update(settingsCaptor.capture()) verify(database.siteSettingsDao()).update(settingsCaptor.capture())
verify(database.validationResultsDao()).update(resultCaptor.capture()) verify(database.validationResultsDao()).update(resultCaptor.capture())
verify(database.retryPolicyDao()).update(retryPolicyCaptor.capture())
// From fillInModel() below // From fillInModel() below
val updatedSettings = MOCK_MODEL_1.settings!!.copy( val updatedSettings = MOCK_MODEL_1.settings!!.copy(
@ -523,11 +293,13 @@ class ViewSiteViewModelTest {
val updatedResult = MOCK_MODEL_1.lastResult!!.copy( val updatedResult = MOCK_MODEL_1.lastResult!!.copy(
status = WAITING status = WAITING
) )
val retryPolicy = retryPolicyCaptor.firstValue
val updatedModel = MOCK_MODEL_1.copy( val updatedModel = MOCK_MODEL_1.copy(
name = "Hello There", name = "Hello There",
url = "https://www.hellothere.com", url = "https://www.hellothere.com",
settings = updatedSettings, settings = updatedSettings,
lastResult = updatedResult lastResult = updatedResult,
retryPolicy = retryPolicy
) )
assertThat(siteCaptor.firstValue).isEqualTo(updatedModel) assertThat(siteCaptor.firstValue).isEqualTo(updatedModel)
@ -541,13 +313,6 @@ class ViewSiteViewModelTest {
fromFinishingJob = false fromFinishingJob = false
) )
onNameError.assertNoValues()
onUrlError.assertNoValues()
onTimeoutError.assertNoValues()
onCheckIntervalError.assertNoValues()
onSearchTermError.assertNoValues()
onScriptError.assertNoValues()
verify(onDone).invoke() verify(onDone).invoke()
} }
@ -619,5 +384,12 @@ class ViewSiteViewModelTest {
validationScript.value = "throw 'Oh no!'" validationScript.value = "throw 'Oh no!'"
checkIntervalValue.value = 24 checkIntervalValue.value = 24
checkIntervalUnit.value = 60000 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")
)
} }
} }

View file

@ -15,6 +15,7 @@ buildscript {
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:' + versions.kotlin classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:' + versions.kotlin
classpath 'com.github.ben-manes:gradle-versions-plugin:' + versions.versionPlugin classpath 'com.github.ben-manes:gradle-versions-plugin:' + versions.versionPlugin
classpath 'io.fabric.tools:gradle:' + versions.fabricPlugin classpath 'io.fabric.tools:gradle:' + versions.fabricPlugin
classpath 'com.google.gms:google-services:' + versions.googleServices
} }
} }
@ -22,7 +23,6 @@ allprojects {
repositories { repositories {
google() google()
jcenter() jcenter()
maven { url "https://dl.bintray.com/drummer-aidan/maven" }
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
} }

View file

@ -12,6 +12,10 @@ android {
versionName versions.publishVersion versionName versions.publishVersion
} }
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
// For Mozilla Rhino // For Mozilla Rhino
lintOptions { lintOptions {
abortOnError false abortOnError false
@ -30,6 +34,7 @@ dependencies {
implementation 'org.mozilla:rhino:' + versions.rhino implementation 'org.mozilla:rhino:' + versions.rhino
api 'com.afollestad:rxkprefs:' + versions.rxkPrefs api 'com.afollestad:rxkprefs:' + versions.rxkPrefs
api "io.reactivex.rxjava2:rxjava:" + versions.rxJava
testImplementation 'junit:junit:' + versions.junit testImplementation 'junit:junit:' + versions.junit
testImplementation 'com.google.truth:truth:' + versions.truth testImplementation 'com.google.truth:truth:' + versions.truth

View file

@ -18,3 +18,10 @@ package com.afollestad.nocknock.utilities.ext
import android.net.Uri import android.net.Uri
fun String.toUri() = Uri.parse(this)!! fun String.toUri() = Uri.parse(this)!!
fun String?.isNotNullOrEmpty(): Boolean {
if (this == null || this == "null") {
return false
}
return !isNullOrEmpty()
}

View file

@ -14,6 +14,10 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
} }
dependencies { dependencies {

View file

@ -181,7 +181,8 @@ class AppDatabaseTest() {
validationMode = STATUS_CODE, validationMode = STATUS_CODE,
validationArgs = null, validationArgs = null,
disabled = false, disabled = false,
networkTimeout = 10000 networkTimeout = 10000,
certificate = null
) )
val newId = settingsDao.insert(model) val newId = settingsDao.insert(model)
assertThat(newId).isEqualTo(1) assertThat(newId).isEqualTo(1)
@ -199,7 +200,8 @@ class AppDatabaseTest() {
validationMode = STATUS_CODE, validationMode = STATUS_CODE,
validationArgs = null, validationArgs = null,
disabled = false, disabled = false,
networkTimeout = 10000 networkTimeout = 10000,
certificate = null
) )
) )
@ -227,7 +229,8 @@ class AppDatabaseTest() {
validationMode = STATUS_CODE, validationMode = STATUS_CODE,
validationArgs = null, validationArgs = null,
disabled = false, disabled = false,
networkTimeout = 10000 networkTimeout = 10000,
certificate = null
) )
) )
@ -428,9 +431,30 @@ class AppDatabaseTest() {
val allSites = db.allSites() val allSites = db.allSites()
assertThat(allSites.size).isEqualTo(3) assertThat(allSites.size).isEqualTo(3)
assertThat(allSites[0]).isEqualTo(MOCK_MODEL_1) assertThat(allSites[0]).isEqualTo(
assertThat(allSites[1]).isEqualTo(MOCK_MODEL_2) MOCK_MODEL_1.copy(
assertThat(allSites[2]).isEqualTo(MOCK_MODEL_3) 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() { @Test fun extension_put_getSite() {
@ -467,10 +491,12 @@ class AppDatabaseTest() {
) )
val updatedHeaders = listOf( val updatedHeaders = listOf(
modelToUpdate.headers.first().copy( modelToUpdate.headers.first().copy(
id = 7,
key = "One", key = "One",
value = "Hello" value = "Hello"
), ),
modelToUpdate.headers.last().copy( modelToUpdate.headers.last().copy(
id = 8,
key = "Two", key = "Two",
value = "Hey" value = "Hey"
) )

View file

@ -35,7 +35,8 @@ fun fakeSettingsModel(
validationMode = validationMode, validationMode = validationMode,
validationArgs = null, validationArgs = null,
disabled = false, disabled = false,
networkTimeout = 10000 networkTimeout = 10000,
certificate = null
) )
fun fakeResultModel( fun fakeResultModel(

View file

@ -3,52 +3,56 @@ ext.versions = [
minSdk : 21, minSdk : 21,
compileSdk : 28, compileSdk : 28,
buildTools : '28.0.3', buildTools : '28.0.3',
publishVersion : '0.8.5', publishVersion : '0.8.8',
publishVersionCode : 39, publishVersionCode : 46,
// Plugins // Plugins
gradlePlugin : '3.2.1', gradlePlugin : '3.4.0',
spotlessPlugin : '3.17.0', spotlessPlugin : '3.22.0',
versionPlugin : '0.20.0', versionPlugin : '0.21.0',
googleServices : '4.2.0',
fabricPlugin : '1.+', fabricPlugin : '1.+',
// Misc // Misc
okHttp : '3.12.1', okHttp : '3.14.1',
rhino : '1.7.10', rhino : '1.7.10',
// Kotlin // Kotlin
kotlin : '1.3.11', kotlin : '1.3.30',
coroutines : '1.1.0', coroutines : '1.2.0',
koin : '1.0.2', koin : '1.0.2',
// Google/AndroidX // Google/AndroidX
androidxAnnotations : '1.0.1', androidxAnnotations : '1.0.2',
androidxCore : '1.0.2', androidxCore : '1.0.2',
androidxRecyclerView: '1.0.0', androidxRecyclerView: '1.0.0',
androidxBrowser : '1.0.0', androidxBrowser : '1.0.0',
googleMaterial : '1.0.0', googleMaterial : '1.0.0',
room : '2.0.0', room : '2.0.0',
lifecycle : '2.0.0', lifecycle : '2.0.0',
firebaseCore : '16.0.8',
// Rx // Rx
rxJava : '2.2.8',
rxBinding : '3.0.0-alpha1', rxBinding : '3.0.0-alpha1',
// afollestad // afollestad
materialDialogs : '2.0.0-rc7', materialDialogs : '2.8.1',
rxkPrefs : '1.2.1', rxkPrefs : '1.2.5',
vvalidator : '0.4.1',
// Debugging // Debugging
timber : '4.7.1', timber : '4.7.1',
fabric : '2.9.8@aar', fabric : '2.9.9@aar',
// Unit testing // Unit testing
junit : '4.12', junit : '4.12',
mockito : '2.23.4', mockito : '2.27.0',
mockitoKotlin : '2.0.0-RC1', mockitoKotlin : '2.1.0',
truth : '0.42', truth : '0.44',
// UI testing // UI testing
androidxTestRunner : '1.1.1', androidxTestRunner : '1.1.1',
androidxTest : '1.1.0', androidxTest : '1.1.0',
archTesting : '2.0.0' archTesting : '2.0.1'
] ]

View file

@ -26,6 +26,7 @@ import com.afollestad.nocknock.data.model.Status.OK
import com.afollestad.nocknock.engine.R import com.afollestad.nocknock.engine.R
import com.afollestad.nocknock.engine.ssl.SslManager import com.afollestad.nocknock.engine.ssl.SslManager
import com.afollestad.nocknock.engine.validation.ValidationJob.Companion.KEY_SITE_ID 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.BundleProvider
import com.afollestad.nocknock.utilities.providers.JobInfoProvider import com.afollestad.nocknock.utilities.providers.JobInfoProvider
import com.afollestad.nocknock.utilities.providers.StringProvider import com.afollestad.nocknock.utilities.providers.StringProvider
@ -35,6 +36,7 @@ import okhttp3.Response
import org.jetbrains.annotations.TestOnly import org.jetbrains.annotations.TestOnly
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.TimeUnit.MILLISECONDS
import kotlin.math.max
import timber.log.Timber.d as log import timber.log.Timber.d as log
/** @author Aidan Follestad (@afollestad) */ /** @author Aidan Follestad (@afollestad) */
@ -153,22 +155,24 @@ class RealValidationExecutor(
check(site.id != 0L) { "Cannot schedule validations for jobs with no ID." } check(site.id != 0L) { "Cannot schedule validations for jobs with no ID." }
val siteSettings = site.settings val siteSettings = site.settings
requireNotNull(siteSettings) { "Site settings must be populated." } 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}") log("performValidation(${site.id}) - GET ${site.url}")
val request = Request.Builder() val request = Request.Builder()
.apply { .apply {
url(site.url) url(site.url)
get() get()
site.headers.forEach { header -> site.headers
addHeader(header.key, header.value) .filter { header -> header.key.isNotNullOrEmpty() }
} .forEach { header ->
addHeader(header.key, header.value)
}
} }
.build() .build()
return try { return try {
val clientWithTimeout = clientTimeoutChanger(okHttpClient, siteSettings.networkTimeout) val timeout = max(siteSettings.networkTimeout, 1)
val client = if (!siteSettings.certificate.isNullOrEmpty()) { val clientWithTimeout = clientTimeoutChanger(okHttpClient, timeout)
val client = if (siteSettings.certificate.isNotNullOrEmpty()) {
sslManager.clientForCertificate( sslManager.clientForCertificate(
certUri = siteSettings.certificate!!, certUri = siteSettings.certificate!!,
siteUri = site.url, siteUri = site.url,
@ -205,6 +209,7 @@ class RealValidationExecutor(
) )
) )
} catch (ex: Exception) { } catch (ex: Exception) {
ex.printStackTrace()
log("performValidation(${site.id}) = Error: ${ex.message}") log("performValidation(${site.id}) = Error: ${ex.message}")
CheckResult(model = site.withStatus(status = ERROR, reason = ex.message)) CheckResult(model = site.withStatus(status = ERROR, reason = ex.message))
} }

View file

@ -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 # 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 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true # org.gradle.parallel=true
android.useAndroidX=true
android.enableJetifier=true

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Before After
Before After

View 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
View 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()
}

View file

@ -18,6 +18,8 @@ dependencies {
implementation project(':common') implementation project(':common')
implementation project(':data') implementation project(':data')
api 'com.afollestad:vvalidator:' + versions.vvalidator
implementation 'androidx.appcompat:appcompat:' + versions.androidxCore implementation 'androidx.appcompat:appcompat:' + versions.androidxCore
implementation 'com.google.android.material:material:' + versions.googleMaterial implementation 'com.google.android.material:material:' + versions.googleMaterial
api 'androidx.lifecycle:lifecycle-extensions:' + versions.lifecycle api 'androidx.lifecycle:lifecycle-extensions:' + versions.lifecycle

View file

@ -17,37 +17,24 @@ package com.afollestad.nocknock.viewcomponents.ext
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewTreeObserver
import androidx.annotation.DimenRes import androidx.annotation.DimenRes
import com.afollestad.vvalidator.form.Condition
fun View.show() { fun View.show() {
visibility = VISIBLE visibility = VISIBLE
} }
fun View.conceal() {
visibility = INVISIBLE
}
fun View.hide() { fun View.hide() {
visibility = GONE visibility = GONE
} }
fun View.showOrHide(show: Boolean) = if (show) show() else hide() 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.dimenFloat(@DimenRes res: Int) = resources.getDimension(res)
fun View.dimenInt(@DimenRes res: Int) = resources.getDimensionPixelSize(res) fun View.dimenInt(@DimenRes res: Int) = resources.getDimensionPixelSize(res)
fun View.isVisibleCondition(): Condition = {
visibility == VISIBLE
}

View file

@ -56,9 +56,22 @@ class HeaderStackLayout(
override fun onClick(v: View) { override fun onClick(v: View) {
val index = v.tag as Int val index = v.tag as Int
list.removeViewAt(index) check(index >= 0 || index < list.childCount) {
headers.removeAt(index) "Index $index is out of bounds in the header stack (size ${list.childCount})."
postLiveData() }
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) { private fun addEntry(forHeader: Header) {
@ -67,9 +80,7 @@ class HeaderStackLayout(
val li = LayoutInflater.from(context) val li = LayoutInflater.from(context)
val entry = li.inflate(R.layout.header_stack_item, list, false) as HeaderItemLayout val entry = li.inflate(R.layout.header_stack_item, list, false) as HeaderItemLayout
list.addView(entry) list.addView(entry.apply {
entry.run {
inputKey.setText(forHeader.key) inputKey.setText(forHeader.key)
inputKey.post { entry.inputKey.requestFocus() } inputKey.post { entry.inputKey.requestFocus() }
attachHeader(forHeader, this@HeaderStackLayout) attachHeader(forHeader, this@HeaderStackLayout)
@ -77,6 +88,6 @@ class HeaderStackLayout(
btnRemove.tag = headers.size - 1 btnRemove.tag = headers.size - 1
btnRemove.setOnClickListener(this@HeaderStackLayout) btnRemove.setOnClickListener(this@HeaderStackLayout)
} })
} }
} }

View file

@ -19,7 +19,6 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.afollestad.nocknock.utilities.ext.DAY import com.afollestad.nocknock.utilities.ext.DAY
import com.afollestad.nocknock.utilities.ext.HOUR 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.R
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
import com.afollestad.nocknock.viewcomponents.livedata.lifecycleOwner 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.input
import kotlinx.android.synthetic.main.validation_interval_layout.view.spinner import kotlinx.android.synthetic.main.validation_interval_layout.view.spinner
@ -66,8 +65,14 @@ class ValidationIntervalLayout(
fun attach( fun attach(
valueData: MutableLiveData<Int>, valueData: MutableLiveData<Int>,
multiplierData: MutableLiveData<Long>, 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) input.attachLiveData(lifecycleOwner(), valueData)
spinner.attachLiveData( spinner.attachLiveData(
lifecycleOwner = lifecycleOwner(), lifecycleOwner = lifecycleOwner(),
@ -91,10 +96,5 @@ class ValidationIntervalLayout(
} }
} }
) )
errorData.toViewError(lifecycleOwner(), this, ::setError)
}
private fun setError(error: String?) {
input.error = error
} }
} }

View file

@ -20,15 +20,17 @@ import android.util.AttributeSet
import android.widget.HorizontalScrollView import android.widget.HorizontalScrollView
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.afollestad.nocknock.viewcomponents.R
import com.afollestad.nocknock.viewcomponents.R.dimen import com.afollestad.nocknock.viewcomponents.R.dimen
import com.afollestad.nocknock.viewcomponents.R.layout import com.afollestad.nocknock.viewcomponents.R.layout
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
import com.afollestad.nocknock.viewcomponents.ext.dimenInt 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.ext.showOrHide
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
import com.afollestad.nocknock.viewcomponents.livedata.lifecycleOwner import com.afollestad.nocknock.viewcomponents.livedata.lifecycleOwner
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility 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.error_text
import kotlinx.android.synthetic.main.javascript_input_layout.view.userInput import kotlinx.android.synthetic.main.javascript_input_layout.view.userInput
@ -50,16 +52,25 @@ class JavaScriptInputLayout(
contentInset // bottom contentInset // bottom
) )
elevation = dimenFloat(dimen.default_elevation) elevation = dimenFloat(dimen.default_elevation)
inflate(context, layout.javascript_input_layout, this) inflate(context, R.layout.javascript_input_layout, this)
} }
fun attach( fun attach(
codeData: MutableLiveData<String>, 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) userInput.attachLiveData(lifecycleOwner(), codeData)
errorData.toViewError(lifecycleOwner(), this, ::setError)
visibility.toViewVisibility(lifecycleOwner(), this) visibility.toViewVisibility(lifecycleOwner(), this)
} }

View file

@ -24,6 +24,7 @@ import com.afollestad.nocknock.viewcomponents.R
import com.afollestad.nocknock.viewcomponents.ext.asSafeInt import com.afollestad.nocknock.viewcomponents.ext.asSafeInt
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
import com.afollestad.nocknock.viewcomponents.livedata.lifecycleOwner 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.minutes
import kotlinx.android.synthetic.main.retry_policy_layout.view.times import kotlinx.android.synthetic.main.retry_policy_layout.view.times
import kotlinx.android.synthetic.main.retry_policy_layout.view.retry_policy_desc as description import kotlinx.android.synthetic.main.retry_policy_layout.view.retry_policy_desc as description
@ -41,7 +42,8 @@ class RetryPolicyLayout(
fun attach( fun attach(
timesData: MutableLiveData<Int>, timesData: MutableLiveData<Int>,
minutesData: MutableLiveData<Int> minutesData: MutableLiveData<Int>,
form: Form
) { ) {
times.attachLiveData(lifecycleOwner(), timesData) times.attachLiveData(lifecycleOwner(), timesData)
minutes.attachLiveData(lifecycleOwner(), minutesData) minutes.attachLiveData(lifecycleOwner(), minutesData)
@ -50,6 +52,13 @@ class RetryPolicyLayout(
minutes.onTextChanged { invalidateDescriptionText() } minutes.onTextChanged { invalidateDescriptionText() }
invalidateDescriptionText() invalidateDescriptionText()
form.input(times, optional = true) {
isNumber().greaterThan(0)
}
form.input(minutes, optional = true) {
isNumber().greaterThan(0)
}
} }
private fun invalidateDescriptionText() { private fun invalidateDescriptionText() {

View file

@ -23,4 +23,8 @@
<string name="header_name">Header Name</string> <string name="header_name">Header Name</string>
<string name="header_value">Header Value</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> </resources>