Compare commits
No commits in common. "master" and "0.8.4" have entirely different histories.
3
.editorconfig
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[*.kt]
|
||||||
|
indent_size = 2
|
||||||
|
continuation_indent_size=4
|
2
.gitignore
vendored
|
@ -181,5 +181,3 @@ gradle-app.setting
|
||||||
|
|
||||||
# # 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
|
@ -40,7 +40,7 @@
|
||||||
</value>
|
</value>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" 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">
|
||||||
|
|
19
.travis.yml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
language: android
|
||||||
|
jdk: oraclejdk8
|
||||||
|
before_script:
|
||||||
|
- echo no | android create avd --force -n test -t android-22 --abi armeabi-v7a
|
||||||
|
- emulator -avd test -no-audio -no-window &
|
||||||
|
- android-wait-for-emulator
|
||||||
|
- adb shell input keyevent 82 &
|
||||||
|
android:
|
||||||
|
components:
|
||||||
|
- tools
|
||||||
|
- platform-tools
|
||||||
|
- build-tools-28.0.3
|
||||||
|
- android-28
|
||||||
|
- extra-android-support
|
||||||
|
- extra-android-m2repository
|
||||||
|
- extra-google-m2repository
|
||||||
|
|
||||||
|
licenses:
|
||||||
|
- '.+'
|
|
@ -1,8 +1,9 @@
|
||||||
## Nock Nock
|
## Nock Nock
|
||||||
|
|
||||||
|
[](https://travis-ci.org/afollestad/nock-nock)
|
||||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Nock Nock is a simple app which allows you to monitor your websites for maximum uptime.
|
Nock Nock is a simple app which allows you to monitor your websites for maximum uptime.
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,18 @@ 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
|
||||||
|
@ -14,15 +26,16 @@ android {
|
||||||
targetSdkVersion versions.compileSdk
|
targetSdkVersion versions.compileSdk
|
||||||
versionCode versions.publishVersionCode
|
versionCode versions.publishVersionCode
|
||||||
versionName versions.publishVersion
|
versionName versions.publishVersion
|
||||||
|
manifestPlaceholders = [fabricKey:getFabricApiKey()]
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
buildTypes {
|
||||||
sourceCompatibility 1.8
|
debug {
|
||||||
targetCompatibility 1.8
|
buildConfigField "String", "FABRIC_API_KEY", "\"\""
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
buildConfigField "String", "FABRIC_API_KEY", "\"${getFabricApiKey()}\""
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
|
||||||
exclude 'META-INF/atomicfu.kotlin_module'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +51,6 @@ 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
|
||||||
|
@ -73,7 +85,3 @@ dependencies {
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: '../spotless.gradle'
|
apply from: '../spotless.gradle'
|
||||||
apply from: '../mock/mock.gradle'
|
|
||||||
|
|
||||||
apply plugin: "io.fabric"
|
|
||||||
apply plugin: 'com.google.gms.google-services'
|
|
|
@ -50,6 +50,9 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="io.fabric.ApiKey"
|
||||||
|
android:value="${fabricKey}"/>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="preloaded_fonts"
|
android:name="preloaded_fonts"
|
||||||
android:resource="@array/preloaded_fonts"/>
|
android:resource="@array/preloaded_fonts"/>
|
||||||
|
|
|
@ -20,12 +20,12 @@ import android.app.Application
|
||||||
import android.app.Application.ActivityLifecycleCallbacks
|
import android.app.Application.ActivityLifecycleCallbacks
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||||
import androidx.core.text.HtmlCompat.fromHtml
|
import androidx.core.text.HtmlCompat.fromHtml
|
||||||
import com.afollestad.materialdialogs.utils.MDUtil.resolveColor
|
import com.afollestad.materialdialogs.utils.MDUtil.resolveColor
|
||||||
import com.afollestad.nocknock.utilities.ext.toUri
|
|
||||||
import com.afollestad.nocknock.utilities.ui.toast
|
import com.afollestad.nocknock.utilities.ui.toast
|
||||||
|
|
||||||
typealias ActivityLifeChange = (activity: Activity, resumed: Boolean) -> Unit
|
typealias ActivityLifeChange = (activity: Activity, resumed: Boolean) -> Unit
|
||||||
|
@ -57,6 +57,8 @@ fun Application.onActivityLifeChange(cb: ActivityLifeChange) {
|
||||||
|
|
||||||
fun String.toHtml() = fromHtml(this, FROM_HTML_MODE_LEGACY)
|
fun String.toHtml() = fromHtml(this, FROM_HTML_MODE_LEGACY)
|
||||||
|
|
||||||
|
fun String.toUri() = Uri.parse(this)!!
|
||||||
|
|
||||||
fun Activity.viewUrl(url: String) {
|
fun Activity.viewUrl(url: String) {
|
||||||
val customTabsIntent = CustomTabsIntent.Builder()
|
val customTabsIntent = CustomTabsIntent.Builder()
|
||||||
.apply {
|
.apply {
|
||||||
|
|
|
@ -47,8 +47,10 @@ 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,
|
||||||
|
|
|
@ -25,7 +25,6 @@ import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.data.Database1to2Migration
|
import com.afollestad.nocknock.data.Database1to2Migration
|
||||||
import com.afollestad.nocknock.data.Database2to3Migration
|
import com.afollestad.nocknock.data.Database2to3Migration
|
||||||
import com.afollestad.nocknock.data.Database3to4Migration
|
import com.afollestad.nocknock.data.Database3to4Migration
|
||||||
import com.afollestad.nocknock.data.Database4to5Migration
|
|
||||||
import com.afollestad.nocknock.notifications.Qualifiers.MAIN_ACTIVITY_CLASS
|
import com.afollestad.nocknock.notifications.Qualifiers.MAIN_ACTIVITY_CLASS
|
||||||
import com.afollestad.nocknock.ui.main.MainActivity
|
import com.afollestad.nocknock.ui.main.MainActivity
|
||||||
import com.afollestad.nocknock.utilities.ext.systemService
|
import com.afollestad.nocknock.utilities.ext.systemService
|
||||||
|
@ -44,8 +43,7 @@ val mainModule = module {
|
||||||
.addMigrations(
|
.addMigrations(
|
||||||
Database1to2Migration(),
|
Database1to2Migration(),
|
||||||
Database2to3Migration(),
|
Database2to3Migration(),
|
||||||
Database3to4Migration(),
|
Database3to4Migration()
|
||||||
Database4to5Migration()
|
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,15 +15,10 @@
|
||||||
*/
|
*/
|
||||||
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
|
||||||
|
@ -40,7 +35,6 @@ abstract class DarkModeSwitchActivity : AppCompatActivity() {
|
||||||
setTheme(themeRes())
|
setTheme(themeRes())
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (getCurrentNightMode() == UNKNOWN) {
|
|
||||||
darkModePref.observe()
|
darkModePref.observe()
|
||||||
.filter { it != isDarkModeEnabled }
|
.filter { it != isDarkModeEnabled }
|
||||||
.subscribe {
|
.subscribe {
|
||||||
|
@ -49,26 +43,8 @@ abstract class DarkModeSwitchActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
.attachLifecycle(this)
|
.attachLifecycle(this)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected fun getCurrentNightMode(): NightMode {
|
protected fun isDarkMode() = darkModePref.get()
|
||||||
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())
|
||||||
|
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
|
@ -16,28 +16,19 @@
|
||||||
package com.afollestad.nocknock.ui.addsite
|
package com.afollestad.nocknock.ui.addsite
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
|
||||||
import android.content.Intent.ACTION_OPEN_DOCUMENT
|
|
||||||
import android.content.Intent.CATEGORY_OPENABLE
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode
|
import com.afollestad.nocknock.data.model.ValidationMode
|
||||||
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
||||||
import com.afollestad.nocknock.ui.viewsite.KEY_SITE
|
import com.afollestad.nocknock.ui.viewsite.KEY_SITE
|
||||||
import com.afollestad.nocknock.utilities.ext.onTextChanged
|
|
||||||
import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
|
|
||||||
import com.afollestad.nocknock.utilities.livedata.distinct
|
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
|
import com.afollestad.nocknock.viewcomponents.ext.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
|
||||||
|
@ -50,8 +41,6 @@ import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchT
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.retryPolicyLayout
|
import kotlinx.android.synthetic.main.activity_addsite.retryPolicyLayout
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout
|
import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.scrollView
|
import kotlinx.android.synthetic.main.activity_addsite.scrollView
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.sslCertificateBrowse
|
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.sslCertificateInput
|
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning
|
import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
|
import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
|
||||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||||
|
@ -61,19 +50,14 @@ import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTi
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class AddSiteActivity : DarkModeSwitchActivity() {
|
class AddSiteActivity : DarkModeSwitchActivity() {
|
||||||
companion object {
|
|
||||||
private const val SELECT_CERT_FILE_RQ = 23
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
@ -86,17 +70,23 @@ 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(
|
||||||
|
@ -105,6 +95,8 @@ 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)
|
||||||
|
|
||||||
|
@ -117,10 +109,25 @@ class AddSiteActivity : DarkModeSwitchActivity() {
|
||||||
viewModel.onValidationSearchTermVisibility()
|
viewModel.onValidationSearchTermVisibility()
|
||||||
.toViewVisibility(this, responseValidationSearchTerm)
|
.toViewVisibility(this, responseValidationSearchTerm)
|
||||||
|
|
||||||
// SSL certificate
|
// Validation script
|
||||||
sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it }
|
scriptInputLayout.attach(
|
||||||
viewModel.certificateUri.distinct()
|
codeData = viewModel.validationScript,
|
||||||
.observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) })
|
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
|
||||||
|
)
|
||||||
|
|
||||||
// Headers
|
// Headers
|
||||||
headersLayout.attach(viewModel.headers)
|
headersLayout.attach(viewModel.headers)
|
||||||
|
@ -130,6 +137,15 @@ 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() }
|
||||||
}
|
}
|
||||||
|
@ -149,87 +165,5 @@ class AddSiteActivity : DarkModeSwitchActivity() {
|
||||||
0f
|
0f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSL certificate
|
|
||||||
sslCertificateBrowse.setOnClickListener {
|
|
||||||
val intent = Intent(ACTION_OPEN_DOCUMENT).apply {
|
|
||||||
addCategory(CATEGORY_OPENABLE)
|
|
||||||
type = "*/*"
|
|
||||||
}
|
|
||||||
startActivityForResult(intent, SELECT_CERT_FILE_RQ)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupValidation() {
|
|
||||||
validationForm = form {
|
|
||||||
input(inputName, name = "Name") {
|
|
||||||
isNotEmpty().description(R.string.please_enter_name)
|
|
||||||
}
|
|
||||||
input(inputUrl, name = "URL") {
|
|
||||||
isNotEmpty().description(R.string.please_enter_url)
|
|
||||||
isUrl().description(R.string.please_enter_valid_url)
|
|
||||||
}
|
|
||||||
input(responseTimeoutInput, name = "Timeout", optional = true) {
|
|
||||||
isNumber().greaterThan(0)
|
|
||||||
.description(R.string.please_enter_networkTimeout)
|
|
||||||
}
|
|
||||||
input(responseValidationSearchTerm, name = "Search term") {
|
|
||||||
conditional(responseValidationSearchTerm.isVisibleCondition()) {
|
|
||||||
isNotEmpty().description(R.string.please_enter_search_term)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
input(sslCertificateInput, name = "Certificate Path", optional = true) {
|
|
||||||
isUri().hasScheme("file", "content")
|
|
||||||
.that { it.host != null }
|
|
||||||
.description(R.string.please_enter_validCertUri)
|
|
||||||
}
|
|
||||||
submitWith(toolbar.menu, R.id.commit) {
|
|
||||||
viewModel.commit {
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation script
|
|
||||||
scriptInputLayout.attach(
|
|
||||||
codeData = viewModel.validationScript,
|
|
||||||
visibility = viewModel.onValidationScriptVisibility(),
|
|
||||||
form = validationForm
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check interval
|
|
||||||
checkIntervalLayout.attach(
|
|
||||||
valueData = viewModel.checkIntervalValue,
|
|
||||||
multiplierData = viewModel.checkIntervalUnit,
|
|
||||||
form = validationForm
|
|
||||||
)
|
|
||||||
|
|
||||||
// Retry Policy
|
|
||||||
retryPolicyLayout.attach(
|
|
||||||
timesData = viewModel.retryPolicyTimes,
|
|
||||||
minutesData = viewModel.retryPolicyMinutes,
|
|
||||||
form = validationForm
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {
|
|
||||||
appToolbar.dimenFloat(R.dimen.default_elevation)
|
|
||||||
} else {
|
|
||||||
0f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(
|
|
||||||
requestCode: Int,
|
|
||||||
resultCode: Int,
|
|
||||||
resultData: Intent?
|
|
||||||
) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, resultData)
|
|
||||||
if (requestCode == SELECT_CERT_FILE_RQ && resultCode == RESULT_OK) {
|
|
||||||
sslCertificateInput.setText(resultData?.data?.toString() ?: "")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ 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.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
|
||||||
|
@ -67,7 +68,6 @@ class AddSiteViewModel(
|
||||||
val retryPolicyTimes = MutableLiveData<Int>()
|
val retryPolicyTimes = MutableLiveData<Int>()
|
||||||
val retryPolicyMinutes = MutableLiveData<Int>()
|
val retryPolicyMinutes = MutableLiveData<Int>()
|
||||||
val headers = MutableLiveData<List<Header>>()
|
val headers = MutableLiveData<List<Header>>()
|
||||||
val certificateUri = MutableLiveData<String>()
|
|
||||||
|
|
||||||
@OnLifecycleEvent(ON_START)
|
@OnLifecycleEvent(ON_START)
|
||||||
fun setDefaults() {
|
fun setDefaults() {
|
||||||
|
@ -81,10 +81,22 @@ 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?>()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
@ -92,6 +104,8 @@ 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!!) {
|
||||||
|
@ -102,9 +116,17 @@ class AddSiteViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@CheckResult fun onValidationSearchTermVisibility() = validationMode.map { it == TERM_SEARCH }
|
@CheckResult fun onValidationSearchTermError(): LiveData<Int?> = validationSearchTermError
|
||||||
|
|
||||||
@CheckResult fun onValidationScriptVisibility() = validationMode.map { it == JAVASCRIPT }
|
@CheckResult fun onValidationSearchTermVisibility() =
|
||||||
|
validationMode.map { it == TERM_SEARCH }
|
||||||
|
|
||||||
|
@CheckResult fun onValidationScriptError(): LiveData<Int?> = validationScriptError
|
||||||
|
|
||||||
|
@CheckResult fun onValidationScriptVisibility() =
|
||||||
|
validationMode.map { it == JAVASCRIPT }
|
||||||
|
|
||||||
|
@CheckResult fun onCheckIntervalError(): LiveData<Int?> = checkIntervalValueError
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
fun commit(done: () -> Unit) {
|
fun commit(done: () -> Unit) {
|
||||||
|
@ -144,7 +166,70 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
@ -152,8 +237,7 @@ class AddSiteViewModel(
|
||||||
validationMode = validationMode.value!!,
|
validationMode = validationMode.value!!,
|
||||||
validationArgs = getValidationArgs(),
|
validationArgs = getValidationArgs(),
|
||||||
networkTimeout = timeout,
|
networkTimeout = timeout,
|
||||||
disabled = false,
|
disabled = false
|
||||||
certificate = certificateUri.value?.toString()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val newLastResult = ValidationResult(
|
val newLastResult = ValidationResult(
|
||||||
|
@ -166,8 +250,7 @@ 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,
|
count = retryPolicyTimes, minutes = retryPolicyMinutes
|
||||||
minutes = retryPolicyMinutes
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|
|
@ -24,6 +24,7 @@ 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
|
||||||
|
@ -32,8 +33,10 @@ 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
|
||||||
|
@ -90,17 +93,12 @@ 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)
|
||||||
.apply {
|
.isChecked = isDarkMode()
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -146,4 +144,20 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,6 @@ package com.afollestad.nocknock.ui.viewsite
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.ACTION_OPEN_DOCUMENT
|
|
||||||
import android.content.Intent.CATEGORY_OPENABLE
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
@ -27,18 +25,13 @@ import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode
|
import com.afollestad.nocknock.data.model.ValidationMode
|
||||||
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
||||||
import com.afollestad.nocknock.utilities.ext.onTextChanged
|
|
||||||
import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
|
|
||||||
import com.afollestad.nocknock.utilities.livedata.distinct
|
|
||||||
import com.afollestad.nocknock.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
|
||||||
|
@ -52,26 +45,20 @@ import kotlinx.android.synthetic.main.activity_viewsite.responseValidationSearch
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.retryPolicyLayout
|
import kotlinx.android.synthetic.main.activity_viewsite.retryPolicyLayout
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.scriptInputLayout
|
import kotlinx.android.synthetic.main.activity_viewsite.scriptInputLayout
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.scrollView
|
import kotlinx.android.synthetic.main.activity_viewsite.scrollView
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.sslCertificateBrowse
|
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.sslCertificateInput
|
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.textLastCheckResult
|
import kotlinx.android.synthetic.main.activity_viewsite.textLastCheckResult
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck
|
import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
|
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription
|
import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription
|
||||||
|
import kotlinx.android.synthetic.main.include_app_bar.app_toolbar as appToolbar
|
||||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import kotlinx.android.synthetic.main.include_app_bar.app_toolbar as appToolbar
|
|
||||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
|
import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class ViewSiteActivity : DarkModeSwitchActivity() {
|
class ViewSiteActivity : DarkModeSwitchActivity() {
|
||||||
companion object {
|
|
||||||
private const val SELECT_CERT_FILE_RQ = 23
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -84,18 +71,17 @@ 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)
|
||||||
|
|
||||||
// Populate view model with initial data
|
|
||||||
val model = intent.getSerializableExtra(KEY_SITE) as Site
|
|
||||||
viewModel.setModel(model)
|
|
||||||
|
|
||||||
setupUi()
|
setupUi()
|
||||||
setupValidation()
|
|
||||||
lifecycle.run {
|
lifecycle.run {
|
||||||
addObserver(viewModel)
|
addObserver(viewModel)
|
||||||
addObserver(statusUpdateReceiver)
|
addObserver(statusUpdateReceiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Populate view model with initial data
|
||||||
|
val model = intent.getSerializableExtra(KEY_SITE) as Site
|
||||||
|
viewModel.setModel(model)
|
||||||
|
|
||||||
// Loading
|
// Loading
|
||||||
loadingProgress.observe(this, viewModel.onIsLoading())
|
loadingProgress.observe(this, viewModel.onIsLoading())
|
||||||
|
|
||||||
|
@ -107,17 +93,23 @@ 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(
|
||||||
|
@ -126,6 +118,8 @@ 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)
|
||||||
|
|
||||||
|
@ -134,10 +128,25 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
|
||||||
viewModel.onValidationSearchTermVisibility()
|
viewModel.onValidationSearchTermVisibility()
|
||||||
.toViewVisibility(this, responseValidationSearchTerm)
|
.toViewVisibility(this, responseValidationSearchTerm)
|
||||||
|
|
||||||
// SSL certificate
|
// Validation script
|
||||||
sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it }
|
scriptInputLayout.attach(
|
||||||
viewModel.certificateUri.distinct()
|
codeData = viewModel.validationScript,
|
||||||
.observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) })
|
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
|
||||||
|
)
|
||||||
|
|
||||||
// Headers
|
// Headers
|
||||||
headersLayout.attach(viewModel.headers)
|
headersLayout.attach(viewModel.headers)
|
||||||
|
@ -164,6 +173,7 @@ 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()
|
||||||
}
|
}
|
||||||
|
@ -194,91 +204,12 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
|
||||||
.isVisible = it
|
.isVisible = it
|
||||||
})
|
})
|
||||||
|
|
||||||
// Done item text
|
// Done button
|
||||||
viewModel.onDoneButtonText()
|
viewModel.onDoneButtonText()
|
||||||
.observe(this, Observer {
|
.observe(this, Observer {
|
||||||
toolbar.menu.findItem(R.id.commit)
|
toolbar.menu.findItem(R.id.commit)
|
||||||
.setTitle(it)
|
.setTitle(it)
|
||||||
})
|
})
|
||||||
|
|
||||||
// SSL certificate
|
|
||||||
sslCertificateBrowse.setOnClickListener {
|
|
||||||
val intent = Intent(ACTION_OPEN_DOCUMENT).apply {
|
|
||||||
addCategory(CATEGORY_OPENABLE)
|
|
||||||
type = "*/*"
|
|
||||||
}
|
|
||||||
startActivityForResult(intent, SELECT_CERT_FILE_RQ)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupValidation() {
|
|
||||||
validationForm = form {
|
|
||||||
input(inputName, name = "Name") {
|
|
||||||
isNotEmpty().description(R.string.please_enter_name)
|
|
||||||
}
|
|
||||||
input(inputUrl, name = "URL") {
|
|
||||||
isNotEmpty().description(R.string.please_enter_url)
|
|
||||||
isUrl().description(R.string.please_enter_valid_url)
|
|
||||||
}
|
|
||||||
input(responseValidationSearchTerm, name = "Search term") {
|
|
||||||
conditional(responseValidationSearchTerm.isVisibleCondition()) {
|
|
||||||
isNotEmpty().description(R.string.please_enter_search_term)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
input(responseTimeoutInput, name = "Timeout", optional = true) {
|
|
||||||
isNumber().greaterThan(0)
|
|
||||||
.description(R.string.please_enter_networkTimeout)
|
|
||||||
}
|
|
||||||
input(sslCertificateInput, name = "Certificate Path", optional = true) {
|
|
||||||
isUri().hasScheme("file", "content")
|
|
||||||
.that { it.host != null }
|
|
||||||
.description(R.string.please_enter_validCertUri)
|
|
||||||
}
|
|
||||||
submitWith(toolbar.menu, R.id.commit) {
|
|
||||||
viewModel.commit { finish() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation script
|
|
||||||
scriptInputLayout.attach(
|
|
||||||
codeData = viewModel.validationScript,
|
|
||||||
visibility = viewModel.onValidationScriptVisibility(),
|
|
||||||
form = validationForm
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check interval
|
|
||||||
checkIntervalLayout.attach(
|
|
||||||
valueData = viewModel.checkIntervalValue,
|
|
||||||
multiplierData = viewModel.checkIntervalUnit,
|
|
||||||
form = validationForm
|
|
||||||
)
|
|
||||||
|
|
||||||
// Retry Policy
|
|
||||||
retryPolicyLayout.attach(
|
|
||||||
timesData = viewModel.retryPolicyTimes,
|
|
||||||
minutesData = viewModel.retryPolicyMinutes,
|
|
||||||
form = validationForm
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {
|
|
||||||
appToolbar.dimenFloat(R.dimen.default_elevation)
|
|
||||||
} else {
|
|
||||||
0f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(
|
|
||||||
requestCode: Int,
|
|
||||||
resultCode: Int,
|
|
||||||
resultData: Intent?
|
|
||||||
) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, resultData)
|
|
||||||
if (requestCode == SELECT_CERT_FILE_RQ && resultCode == RESULT_OK) {
|
|
||||||
sslCertificateInput.setText(resultData?.data?.toString() ?: "")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
|
|
@ -23,9 +23,9 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.AppDatabase
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
|
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||||
import com.afollestad.nocknock.data.deleteSite
|
import com.afollestad.nocknock.data.deleteSite
|
||||||
import com.afollestad.nocknock.data.model.Header
|
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.Status
|
import com.afollestad.nocknock.data.model.Status
|
||||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||||
|
@ -43,6 +43,7 @@ import com.afollestad.nocknock.utilities.ext.formatDate
|
||||||
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
|
||||||
|
@ -75,14 +76,25 @@ class ViewSiteViewModel(
|
||||||
val retryPolicyTimes = MutableLiveData<Int>()
|
val retryPolicyTimes = MutableLiveData<Int>()
|
||||||
val retryPolicyMinutes = MutableLiveData<Int>()
|
val retryPolicyMinutes = MutableLiveData<Int>()
|
||||||
val headers = MutableLiveData<List<Header>>()
|
val headers = MutableLiveData<List<Header>>()
|
||||||
val certificateUri = MutableLiveData<String>()
|
|
||||||
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?>()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
@ -90,6 +102,8 @@ 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!!) {
|
||||||
|
@ -100,11 +114,20 @@ class ViewSiteViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@CheckResult fun onValidationSearchTermVisibility() = validationMode.map { it == TERM_SEARCH }
|
@CheckResult fun onValidationSearchTermError(): LiveData<Int?> = validationSearchTermError
|
||||||
|
|
||||||
@CheckResult fun onValidationScriptVisibility() = validationMode.map { it == JAVASCRIPT }
|
@CheckResult fun onValidationSearchTermVisibility() =
|
||||||
|
validationMode.map { it == TERM_SEARCH }
|
||||||
|
|
||||||
@CheckResult fun onDisableChecksVisibility(): LiveData<Boolean> = disabled.map { !it }
|
@CheckResult fun onValidationScriptError(): LiveData<Int?> = validationScriptError
|
||||||
|
|
||||||
|
@CheckResult fun onValidationScriptVisibility() =
|
||||||
|
validationMode.map { it == JAVASCRIPT }
|
||||||
|
|
||||||
|
@CheckResult fun onCheckIntervalError(): LiveData<Int?> = checkIntervalValueError
|
||||||
|
|
||||||
|
@CheckResult fun onDisableChecksVisibility(): LiveData<Boolean> =
|
||||||
|
disabled.map { !it }
|
||||||
|
|
||||||
@CheckResult fun onDoneButtonText(): LiveData<Int> =
|
@CheckResult fun onDoneButtonText(): LiveData<Int> =
|
||||||
disabled.map {
|
disabled.map {
|
||||||
|
@ -222,16 +245,78 @@ 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
|
||||||
val cleanedTags = tags.value?.split(',')?.joinToString(separator = ",") ?: ""
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
validationMode = validationMode.value!!,
|
validationMode = validationMode.value!!,
|
||||||
validationArgs = getValidationArgs(),
|
validationArgs = getValidationArgs(),
|
||||||
networkTimeout = timeout,
|
networkTimeout = timeout,
|
||||||
disabled = false,
|
disabled = false
|
||||||
certificate = certificateUri.value?.toString()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val retryPolicyTimes = retryPolicyTimes.value ?: 0
|
val retryPolicyTimes = retryPolicyTimes.value ?: 0
|
||||||
|
@ -239,15 +324,11 @@ 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(
|
site.retryPolicy!!.copy(count = retryPolicyTimes, minutes = retryPolicyMinutes)
|
||||||
count = retryPolicyTimes,
|
|
||||||
minutes = retryPolicyMinutes
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
// Create new policy
|
// Create new policy
|
||||||
RetryPolicy(
|
RetryPolicy(
|
||||||
count = retryPolicyTimes,
|
count = retryPolicyTimes, minutes = retryPolicyMinutes
|
||||||
minutes = retryPolicyMinutes
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -55,11 +55,6 @@ 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
|
||||||
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
|
||||||
|
|
|
@ -91,6 +91,20 @@
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
android:layout_marginTop="@dimen/content_inset"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:text="@string/response_timeout"
|
||||||
|
style="@style/InputForm.Header"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/responseTimeoutInput"
|
||||||
|
android:hint="@string/response_timeout_default"
|
||||||
|
android:inputType="number"
|
||||||
|
android:maxLength="8"
|
||||||
|
tools:ignore="Autofill"
|
||||||
|
style="@style/InputForm.Field"
|
||||||
|
/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/responseValidationLabel"
|
android:id="@+id/responseValidationLabel"
|
||||||
android:text="@string/response_validation_mode"
|
android:text="@string/response_validation_mode"
|
||||||
|
@ -123,7 +137,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="?scriptLayoutBackground"
|
android:background="@color/lighterGray"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -145,59 +159,6 @@
|
||||||
android:layout_marginTop="@dimen/content_inset_more"
|
android:layout_marginTop="@dimen/content_inset_more"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
|
||||||
android:text="@string/response_timeout"
|
|
||||||
style="@style/InputForm.Header"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/responseTimeoutInput"
|
|
||||||
android:hint="@string/response_timeout_default"
|
|
||||||
android:inputType="number"
|
|
||||||
android:maxLength="8"
|
|
||||||
tools:ignore="Autofill"
|
|
||||||
style="@style/InputForm.Field"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
|
||||||
android:text="@string/ssl_certificate"
|
|
||||||
style="@style/NockText.SectionHeader"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
>
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/sslCertificateInput"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="start|center_vertical"
|
|
||||||
android:layout_marginStart="-4dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:hint="@string/ssl_certificate_automatic"
|
|
||||||
android:inputType="textUri"
|
|
||||||
tools:ignore="Autofill,HardcodedText,LabelFor"
|
|
||||||
style="@style/NockText.Body"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/sslCertificateBrowse"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="end|center_vertical"
|
|
||||||
android:text="@string/ssl_certificate_browse"
|
|
||||||
style="@style/AccentTextButton"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<include layout="@layout/include_divider"/>
|
<include layout="@layout/include_divider"/>
|
||||||
|
|
||||||
<com.afollestad.nocknock.viewcomponents.headers.HeaderStackLayout
|
<com.afollestad.nocknock.viewcomponents.headers.HeaderStackLayout
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingBottom="@dimen/content_inset_double"
|
android:paddingBottom="@dimen/content_inset"
|
||||||
android:paddingLeft="@dimen/content_inset"
|
android:paddingLeft="@dimen/content_inset"
|
||||||
android:paddingRight="@dimen/content_inset"
|
android:paddingRight="@dimen/content_inset"
|
||||||
android:paddingTop="@dimen/content_inset_less"
|
android:paddingTop="@dimen/content_inset_less"
|
||||||
|
@ -126,6 +126,35 @@
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
android:layout_marginTop="@dimen/content_inset"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/content_inset_less"
|
||||||
|
android:text="@string/response_timeout"
|
||||||
|
style="@style/NockText.SectionHeader"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/responseTimeoutInput"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="-4dp"
|
||||||
|
android:layout_marginStart="-4dp"
|
||||||
|
android:layout_marginTop="@dimen/content_inset_quarter"
|
||||||
|
android:hint="@string/response_timeout_default"
|
||||||
|
android:inputType="number"
|
||||||
|
android:maxLength="8"
|
||||||
|
tools:ignore="Autofill,HardcodedText,LabelFor"
|
||||||
|
style="@style/NockText.Body"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginTop="@dimen/content_inset_less"
|
||||||
|
android:background="?dividerColor"
|
||||||
|
/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/responseValidationLabel"
|
android:id="@+id/responseValidationLabel"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -161,7 +190,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="?scriptLayoutBackground"
|
android:background="@color/lighterGray"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -188,66 +217,6 @@
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
android:layout_marginTop="@dimen/content_inset"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
|
||||||
android:text="@string/response_timeout"
|
|
||||||
style="@style/NockText.SectionHeader"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/responseTimeoutInput"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="-4dp"
|
|
||||||
android:layout_marginStart="-4dp"
|
|
||||||
android:layout_marginTop="@dimen/content_inset_quarter"
|
|
||||||
android:hint="@string/response_timeout_default"
|
|
||||||
android:inputType="number"
|
|
||||||
android:maxLength="8"
|
|
||||||
tools:ignore="Autofill,HardcodedText,LabelFor"
|
|
||||||
style="@style/NockText.Body"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
|
||||||
android:text="@string/ssl_certificate"
|
|
||||||
style="@style/NockText.SectionHeader"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
>
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/sslCertificateInput"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="start|center_vertical"
|
|
||||||
android:layout_marginStart="-4dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:hint="@string/ssl_certificate_automatic"
|
|
||||||
android:inputType="textUri"
|
|
||||||
tools:ignore="Autofill,HardcodedText,LabelFor"
|
|
||||||
style="@style/NockText.Body"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/sslCertificateBrowse"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="end|center_vertical"
|
|
||||||
android:text="@string/ssl_certificate_browse"
|
|
||||||
style="@style/AccentTextButton"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<include layout="@layout/include_divider"/>
|
<include layout="@layout/include_divider"/>
|
||||||
|
|
||||||
<com.afollestad.nocknock.viewcomponents.headers.HeaderStackLayout
|
<com.afollestad.nocknock.viewcomponents.headers.HeaderStackLayout
|
||||||
|
|
|
@ -7,4 +7,7 @@
|
||||||
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>
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 7 KiB |
Before Width: | Height: | Size: 9 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 15 KiB |
|
@ -13,4 +13,10 @@
|
||||||
<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>
|
||||||
|
|
|
@ -4,6 +4,5 @@
|
||||||
<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>
|
||||||
|
|
|
@ -7,9 +7,7 @@
|
||||||
<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="darkerGray">#303030</color>
|
<color name="lighterGray">#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>
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#758F9A</color>
|
|
||||||
</resources>
|
|
|
@ -14,7 +14,6 @@
|
||||||
<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>
|
||||||
|
|
||||||
|
@ -30,9 +29,10 @@
|
||||||
<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="options">Options</string>
|
<string name="options">Options</string>
|
||||||
<string name="remove_site">Remove Site</string>
|
<string name="remove_site">Remove Site</string>
|
||||||
|
@ -59,10 +59,6 @@
|
||||||
<string name="response_timeout">Network Response Timeout (ms)</string>
|
<string name="response_timeout">Network Response Timeout (ms)</string>
|
||||||
<string name="response_timeout_default">10000</string>
|
<string name="response_timeout_default">10000</string>
|
||||||
|
|
||||||
<string name="ssl_certificate">SSL Certificate</string>
|
|
||||||
<string name="ssl_certificate_automatic">(Automatic)</string>
|
|
||||||
<string name="ssl_certificate_browse">Browse</string>
|
|
||||||
|
|
||||||
<string name="refresh_status">Refresh Status</string>
|
<string name="refresh_status">Refresh Status</string>
|
||||||
|
|
||||||
<string name="warning_http_url">
|
<string name="warning_http_url">
|
||||||
|
@ -85,6 +81,14 @@
|
||||||
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>
|
||||||
|
|
|
@ -4,9 +4,19 @@
|
||||||
|
|
||||||
<style name="AppTheme.Dark" parent="AppThemeParent.Dark"/>
|
<style name="AppTheme.Dark" parent="AppThemeParent.Dark"/>
|
||||||
|
|
||||||
|
<style name="AccentButton" parent="Widget.MaterialComponents.Button">
|
||||||
|
<item name="android:textColor">#fff</item>
|
||||||
|
<item name="backgroundTint">@color/colorAccent</item>
|
||||||
|
<item name="android:fontFamily">@font/lato</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<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/darkerGray</item>
|
<item name="backgroundTint">@color/lighterGray</item>
|
||||||
|
<item name="android:fontFamily">@font/lato</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AccentTextButton" parent="Widget.MaterialComponents.Button.TextButton">
|
||||||
<item name="android:fontFamily">@font/lato</item>
|
<item name="android:fontFamily">@font/lato</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
<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>
|
||||||
|
@ -34,7 +33,6 @@
|
||||||
<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>
|
||||||
|
|
|
@ -19,13 +19,12 @@ import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import com.afollestad.nocknock.data.AppDatabase
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.data.HeaderDao
|
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||||
import com.afollestad.nocknock.data.RetryPolicyDao
|
import com.afollestad.nocknock.data.RetryPolicyDao
|
||||||
import com.afollestad.nocknock.data.SiteDao
|
import com.afollestad.nocknock.data.SiteDao
|
||||||
import com.afollestad.nocknock.data.SiteSettingsDao
|
import com.afollestad.nocknock.data.SiteSettingsDao
|
||||||
import com.afollestad.nocknock.data.ValidationResultsDao
|
import com.afollestad.nocknock.data.ValidationResultsDao
|
||||||
import com.afollestad.nocknock.data.model.Header
|
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
|
import com.afollestad.nocknock.data.model.Status
|
||||||
|
@ -57,8 +56,7 @@ fun fakeSettingsModel(
|
||||||
validationMode = validationMode,
|
validationMode = validationMode,
|
||||||
validationArgs = null,
|
validationArgs = null,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
networkTimeout = 10000,
|
networkTimeout = 10000
|
||||||
certificate = null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fun fakeResultModel(
|
fun fakeResultModel(
|
||||||
|
@ -89,24 +87,20 @@ fun fakeHeaders(siteId: Long): List<Header> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fakeModel(
|
fun fakeModel(id: Long) = Site(
|
||||||
id: Long,
|
|
||||||
tags: String = ""
|
|
||||||
) = Site(
|
|
||||||
id = id,
|
id = id,
|
||||||
name = "Test",
|
name = "Test",
|
||||||
url = "https://test.com",
|
url = "https://test.com",
|
||||||
tags = tags,
|
tags = "",
|
||||||
settings = fakeSettingsModel(id),
|
settings = fakeSettingsModel(id),
|
||||||
lastResult = fakeResultModel(id),
|
lastResult = fakeResultModel(id),
|
||||||
retryPolicy = fakeRetryPolicy(id),
|
retryPolicy = fakeRetryPolicy(id),
|
||||||
headers = fakeHeaders(id)
|
headers = fakeHeaders(id)
|
||||||
)
|
)
|
||||||
|
|
||||||
val MOCK_MODEL_1 = fakeModel(1, tags = "one,two")
|
val MOCK_MODEL_1 = fakeModel(1)
|
||||||
val MOCK_MODEL_2 = fakeModel(2, tags = "three,four")
|
val MOCK_MODEL_2 = fakeModel(2)
|
||||||
val MOCK_MODEL_3 = fakeModel(3, tags = "five,six")
|
val MOCK_MODEL_3 = fakeModel(3)
|
||||||
|
|
||||||
val ALL_MOCK_MODELS = listOf(MOCK_MODEL_1, MOCK_MODEL_2, MOCK_MODEL_3)
|
val ALL_MOCK_MODELS = listOf(MOCK_MODEL_1, MOCK_MODEL_2, MOCK_MODEL_3)
|
||||||
|
|
||||||
fun mockDatabase(): AppDatabase {
|
fun mockDatabase(): AppDatabase {
|
||||||
|
@ -171,29 +165,12 @@ fun mockDatabase(): AppDatabase {
|
||||||
on { update(isA()) } doReturn 1
|
on { update(isA()) } doReturn 1
|
||||||
on { delete(isA()) } doReturn 1
|
on { delete(isA()) } doReturn 1
|
||||||
}
|
}
|
||||||
val headerDao = mock<HeaderDao> {
|
|
||||||
on { all() } doReturn MOCK_MODEL_1.headers + MOCK_MODEL_2.headers + MOCK_MODEL_3.headers
|
|
||||||
on { forSite(isA()) } doAnswer { inv ->
|
|
||||||
val id = inv.getArgument<Long>(0)
|
|
||||||
return@doAnswer when (id) {
|
|
||||||
1L -> MOCK_MODEL_1.headers
|
|
||||||
2L -> MOCK_MODEL_2.headers
|
|
||||||
3L -> MOCK_MODEL_3.headers
|
|
||||||
else -> listOf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
on { insert(isA<Header>()) } doReturn 1L
|
|
||||||
on { insert(isA<List<Header>>()) } doReturn listOf(1L, 2L)
|
|
||||||
on { update(isA()) } doReturn 1
|
|
||||||
on { delete(isA()) } doReturn 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return mock {
|
return mock {
|
||||||
on { siteDao() } doReturn siteDao
|
on { siteDao() } doReturn siteDao
|
||||||
on { siteSettingsDao() } doReturn settingsDao
|
on { siteSettingsDao() } doReturn settingsDao
|
||||||
on { validationResultsDao() } doReturn resultsDao
|
on { validationResultsDao() } doReturn resultsDao
|
||||||
on { retryPolicyDao() } doReturn retryDao
|
on { retryPolicyDao() } doReturn retryDao
|
||||||
on { headerDao() } doReturn headerDao
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,21 +17,20 @@ 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
|
||||||
|
@ -150,9 +149,247 @@ 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>()
|
||||||
|
@ -160,30 +397,31 @@ 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()).insert(validationResultCaptor.capture())
|
verify(database.validationResultsDao(), never()).insert(any())
|
||||||
|
|
||||||
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 = result
|
lastResult = null
|
||||||
)
|
)
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
@ -197,10 +435,5 @@ 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")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,45 +60,18 @@ class MainViewModelTest {
|
||||||
.test()
|
.test()
|
||||||
val sites = viewModel.onSites()
|
val sites = viewModel.onSites()
|
||||||
.test()
|
.test()
|
||||||
val tags = viewModel.onTags()
|
|
||||||
.test()
|
|
||||||
val tagsVisibility = viewModel.onTagsListVisibility()
|
|
||||||
.test()
|
|
||||||
|
|
||||||
viewModel.onResume()
|
viewModel.onResume()
|
||||||
|
|
||||||
verify(notificationManager).cancelStatusNotifications()
|
verify(notificationManager).cancelStatusNotifications()
|
||||||
verify(validationManager).ensureScheduledValidations()
|
verify(validationManager).ensureScheduledValidations()
|
||||||
|
|
||||||
sites.assertValues(ALL_MOCK_MODELS)
|
sites.assertValues(
|
||||||
|
listOf(),
|
||||||
|
ALL_MOCK_MODELS
|
||||||
|
)
|
||||||
isLoading.assertValues(true, false)
|
isLoading.assertValues(true, false)
|
||||||
emptyTextVisibility.assertValues(false, false)
|
emptyTextVisibility.assertValues(false, false)
|
||||||
tags.assertValues(listOf("one", "two", "three", "four", "five", "six").sorted())
|
|
||||||
tagsVisibility.assertValues(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun onTagSelection() = runBlocking {
|
|
||||||
val isLoading = viewModel.onIsLoading()
|
|
||||||
.test()
|
|
||||||
val emptyTextVisibility = viewModel.onEmptyTextVisibility()
|
|
||||||
.test()
|
|
||||||
val sites = viewModel.onSites()
|
|
||||||
.test()
|
|
||||||
val tags = viewModel.onTags()
|
|
||||||
.test()
|
|
||||||
val tagsVisibility = viewModel.onTagsListVisibility()
|
|
||||||
.test()
|
|
||||||
|
|
||||||
viewModel.onTagSelection(listOf("four", "six"))
|
|
||||||
|
|
||||||
verify(notificationManager).cancelStatusNotifications()
|
|
||||||
verify(validationManager).ensureScheduledValidations()
|
|
||||||
|
|
||||||
sites.assertValues(listOf(MOCK_MODEL_2, MOCK_MODEL_3))
|
|
||||||
isLoading.assertValues(true, false)
|
|
||||||
emptyTextVisibility.assertValues(false, false)
|
|
||||||
tags.assertValues(listOf("one", "two", "three", "four", "five", "six").sorted())
|
|
||||||
tagsVisibility.assertValues(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun postSiteUpdate_notFound() {
|
@Test fun postSiteUpdate_notFound() {
|
||||||
|
@ -113,7 +86,10 @@ class MainViewModelTest {
|
||||||
.test()
|
.test()
|
||||||
|
|
||||||
viewModel.onResume()
|
viewModel.onResume()
|
||||||
sites.assertValues(ALL_MOCK_MODELS)
|
sites.assertValues(
|
||||||
|
listOf(),
|
||||||
|
ALL_MOCK_MODELS
|
||||||
|
)
|
||||||
|
|
||||||
val updatedModel2 = MOCK_MODEL_2.copy(
|
val updatedModel2 = MOCK_MODEL_2.copy(
|
||||||
name = "Wakanda Forever!!!"
|
name = "Wakanda Forever!!!"
|
||||||
|
@ -144,7 +120,10 @@ class MainViewModelTest {
|
||||||
.test()
|
.test()
|
||||||
|
|
||||||
viewModel.onResume()
|
viewModel.onResume()
|
||||||
sites.assertValues(ALL_MOCK_MODELS)
|
sites.assertValues(
|
||||||
|
listOf(),
|
||||||
|
ALL_MOCK_MODELS
|
||||||
|
)
|
||||||
isLoading.assertValues(true, false)
|
isLoading.assertValues(true, false)
|
||||||
|
|
||||||
val modifiedModel = MOCK_MODEL_1.copy(id = 11111)
|
val modifiedModel = MOCK_MODEL_1.copy(id = 11111)
|
||||||
|
@ -168,7 +147,10 @@ class MainViewModelTest {
|
||||||
.test()
|
.test()
|
||||||
|
|
||||||
viewModel.onResume()
|
viewModel.onResume()
|
||||||
sites.assertValues(ALL_MOCK_MODELS)
|
sites.assertValues(
|
||||||
|
listOf(),
|
||||||
|
ALL_MOCK_MODELS
|
||||||
|
)
|
||||||
isLoading.assertValues(true, false)
|
isLoading.assertValues(true, false)
|
||||||
|
|
||||||
val modelsWithout1 = ALL_MOCK_MODELS.toMutableList()
|
val modelsWithout1 = ALL_MOCK_MODELS.toMutableList()
|
||||||
|
|
|
@ -18,8 +18,6 @@ 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
|
||||||
|
@ -31,7 +29,6 @@ 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
|
||||||
|
@ -41,10 +38,9 @@ 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
|
||||||
|
@ -259,11 +255,247 @@ class ViewSiteViewModelTest {
|
||||||
.isEqualTo("Two")
|
.isEqualTo("Two")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun commit_success() = runBlocking {
|
@Test fun commit_nameError() {
|
||||||
whenever(database.retryPolicyDao().forSite(any())).doReturn(listOf(fakeRetryPolicy(1)))
|
val onNameError = viewModel.onNameError()
|
||||||
|
.test()
|
||||||
|
val onUrlError = viewModel.onUrlError()
|
||||||
|
.test()
|
||||||
|
val onTimeoutError = viewModel.onTimeoutError()
|
||||||
|
.test()
|
||||||
|
val onCheckIntervalError = viewModel.onCheckIntervalError()
|
||||||
|
.test()
|
||||||
|
val onSearchTermError = viewModel.onValidationSearchTermError()
|
||||||
|
.test()
|
||||||
|
val onScriptError = viewModel.onValidationScriptError()
|
||||||
|
.test()
|
||||||
|
|
||||||
|
fillInModel().apply {
|
||||||
|
name.value = ""
|
||||||
|
}
|
||||||
|
val onDone = mock<() -> Unit>()
|
||||||
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
|
verify(validationManager, never())
|
||||||
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
|
onNameError.assertValues(R.string.please_enter_name)
|
||||||
|
onUrlError.assertNoValues()
|
||||||
|
onTimeoutError.assertNoValues()
|
||||||
|
onCheckIntervalError.assertNoValues()
|
||||||
|
onSearchTermError.assertNoValues()
|
||||||
|
onScriptError.assertNoValues()
|
||||||
|
|
||||||
|
verify(onDone, never()).invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun commit_urlEmptyError() {
|
||||||
|
val onNameError = viewModel.onNameError()
|
||||||
|
.test()
|
||||||
|
val onUrlError = viewModel.onUrlError()
|
||||||
|
.test()
|
||||||
|
val onTimeoutError = viewModel.onTimeoutError()
|
||||||
|
.test()
|
||||||
|
val onCheckIntervalError = viewModel.onCheckIntervalError()
|
||||||
|
.test()
|
||||||
|
val onSearchTermError = viewModel.onValidationSearchTermError()
|
||||||
|
.test()
|
||||||
|
val onScriptError = viewModel.onValidationScriptError()
|
||||||
|
.test()
|
||||||
|
|
||||||
|
fillInModel().apply {
|
||||||
|
url.value = ""
|
||||||
|
}
|
||||||
|
val onDone = mock<() -> Unit>()
|
||||||
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
|
verify(validationManager, never())
|
||||||
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
|
onNameError.assertNoValues()
|
||||||
|
onUrlError.assertValues(R.string.please_enter_url)
|
||||||
|
onTimeoutError.assertNoValues()
|
||||||
|
onCheckIntervalError.assertNoValues()
|
||||||
|
onSearchTermError.assertNoValues()
|
||||||
|
onScriptError.assertNoValues()
|
||||||
|
|
||||||
|
verify(onDone, never()).invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun commit_urlFormatError() {
|
||||||
|
val onNameError = viewModel.onNameError()
|
||||||
|
.test()
|
||||||
|
val onUrlError = viewModel.onUrlError()
|
||||||
|
.test()
|
||||||
|
val onTimeoutError = viewModel.onTimeoutError()
|
||||||
|
.test()
|
||||||
|
val onCheckIntervalError = viewModel.onCheckIntervalError()
|
||||||
|
.test()
|
||||||
|
val onSearchTermError = viewModel.onValidationSearchTermError()
|
||||||
|
.test()
|
||||||
|
val onScriptError = viewModel.onValidationScriptError()
|
||||||
|
.test()
|
||||||
|
|
||||||
|
fillInModel().apply {
|
||||||
|
url.value = "ftp://www.idk.com"
|
||||||
|
}
|
||||||
|
val onDone = mock<() -> Unit>()
|
||||||
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
|
verify(validationManager, never())
|
||||||
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
|
onNameError.assertNoValues()
|
||||||
|
onUrlError.assertValues(R.string.please_enter_valid_url)
|
||||||
|
onTimeoutError.assertNoValues()
|
||||||
|
onCheckIntervalError.assertNoValues()
|
||||||
|
onSearchTermError.assertNoValues()
|
||||||
|
onScriptError.assertNoValues()
|
||||||
|
|
||||||
|
verify(onDone, never()).invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun commit_networkTimeout_error() {
|
||||||
|
val onNameError = viewModel.onNameError()
|
||||||
|
.test()
|
||||||
|
val onUrlError = viewModel.onUrlError()
|
||||||
|
.test()
|
||||||
|
val onTimeoutError = viewModel.onTimeoutError()
|
||||||
|
.test()
|
||||||
|
val onCheckIntervalError = viewModel.onCheckIntervalError()
|
||||||
|
.test()
|
||||||
|
val onSearchTermError = viewModel.onValidationSearchTermError()
|
||||||
|
.test()
|
||||||
|
val onScriptError = viewModel.onValidationScriptError()
|
||||||
|
.test()
|
||||||
|
|
||||||
|
fillInModel().apply {
|
||||||
|
timeout.value = 0
|
||||||
|
}
|
||||||
|
val onDone = mock<() -> Unit>()
|
||||||
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
|
verify(validationManager, never())
|
||||||
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
|
onNameError.assertNoValues()
|
||||||
|
onUrlError.assertNoValues()
|
||||||
|
onTimeoutError.assertValues(R.string.please_enter_networkTimeout)
|
||||||
|
onCheckIntervalError.assertNoValues()
|
||||||
|
onSearchTermError.assertNoValues()
|
||||||
|
onScriptError.assertNoValues()
|
||||||
|
|
||||||
|
verify(onDone, never()).invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun commit_checkIntervalError() {
|
||||||
|
val onNameError = viewModel.onNameError()
|
||||||
|
.test()
|
||||||
|
val onUrlError = viewModel.onUrlError()
|
||||||
|
.test()
|
||||||
|
val onTimeoutError = viewModel.onTimeoutError()
|
||||||
|
.test()
|
||||||
|
val onCheckIntervalError = viewModel.onCheckIntervalError()
|
||||||
|
.test()
|
||||||
|
val onSearchTermError = viewModel.onValidationSearchTermError()
|
||||||
|
.test()
|
||||||
|
val onScriptError = viewModel.onValidationScriptError()
|
||||||
|
.test()
|
||||||
|
|
||||||
|
fillInModel().apply {
|
||||||
|
checkIntervalValue.value = 0
|
||||||
|
}
|
||||||
|
val onDone = mock<() -> Unit>()
|
||||||
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
|
verify(validationManager, never())
|
||||||
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
|
onNameError.assertNoValues()
|
||||||
|
onUrlError.assertNoValues()
|
||||||
|
onTimeoutError.assertNoValues()
|
||||||
|
onCheckIntervalError.assertValues(R.string.please_enter_check_interval)
|
||||||
|
onSearchTermError.assertNoValues()
|
||||||
|
onScriptError.assertNoValues()
|
||||||
|
|
||||||
|
verify(onDone, never()).invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun commit_termSearchError() {
|
||||||
|
val onNameError = viewModel.onNameError()
|
||||||
|
.test()
|
||||||
|
val onUrlError = viewModel.onUrlError()
|
||||||
|
.test()
|
||||||
|
val onTimeoutError = viewModel.onTimeoutError()
|
||||||
|
.test()
|
||||||
|
val onCheckIntervalError = viewModel.onCheckIntervalError()
|
||||||
|
.test()
|
||||||
|
val onSearchTermError = viewModel.onValidationSearchTermError()
|
||||||
|
.test()
|
||||||
|
val onScriptError = viewModel.onValidationScriptError()
|
||||||
|
.test()
|
||||||
|
|
||||||
|
fillInModel().apply {
|
||||||
|
validationMode.value = TERM_SEARCH
|
||||||
|
validationSearchTerm.value = ""
|
||||||
|
}
|
||||||
|
val onDone = mock<() -> Unit>()
|
||||||
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
|
verify(validationManager, never())
|
||||||
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
|
onNameError.assertNoValues()
|
||||||
|
onUrlError.assertNoValues()
|
||||||
|
onTimeoutError.assertNoValues()
|
||||||
|
onCheckIntervalError.assertNoValues()
|
||||||
|
onSearchTermError.assertValues(R.string.please_enter_search_term)
|
||||||
|
onScriptError.assertNoValues()
|
||||||
|
|
||||||
|
verify(onDone, never()).invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun commit_javaScript_error() {
|
||||||
|
val onNameError = viewModel.onNameError()
|
||||||
|
.test()
|
||||||
|
val onUrlError = viewModel.onUrlError()
|
||||||
|
.test()
|
||||||
|
val onTimeoutError = viewModel.onTimeoutError()
|
||||||
|
.test()
|
||||||
|
val onCheckIntervalError = viewModel.onCheckIntervalError()
|
||||||
|
.test()
|
||||||
|
val onSearchTermError = viewModel.onValidationSearchTermError()
|
||||||
|
.test()
|
||||||
|
val onScriptError = viewModel.onValidationScriptError()
|
||||||
|
.test()
|
||||||
|
|
||||||
|
fillInModel().apply {
|
||||||
|
validationMode.value = JAVASCRIPT
|
||||||
|
validationScript.value = ""
|
||||||
|
}
|
||||||
|
val onDone = mock<() -> Unit>()
|
||||||
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
|
verify(validationManager, never())
|
||||||
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
|
onNameError.assertNoValues()
|
||||||
|
onUrlError.assertNoValues()
|
||||||
|
onTimeoutError.assertNoValues()
|
||||||
|
onCheckIntervalError.assertNoValues()
|
||||||
|
onSearchTermError.assertNoValues()
|
||||||
|
onScriptError.assertValues(R.string.please_enter_javaScript)
|
||||||
|
|
||||||
|
verify(onDone, never()).invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun commit_success() = runBlocking {
|
||||||
val isLoading = viewModel.onIsLoading()
|
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>()
|
||||||
|
@ -274,13 +506,11 @@ 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(
|
||||||
|
@ -293,13 +523,11 @@ 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)
|
||||||
|
@ -313,6 +541,13 @@ class ViewSiteViewModelTest {
|
||||||
fromFinishingJob = false
|
fromFinishingJob = false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
onNameError.assertNoValues()
|
||||||
|
onUrlError.assertNoValues()
|
||||||
|
onTimeoutError.assertNoValues()
|
||||||
|
onCheckIntervalError.assertNoValues()
|
||||||
|
onSearchTermError.assertNoValues()
|
||||||
|
onScriptError.assertNoValues()
|
||||||
|
|
||||||
verify(onDone).invoke()
|
verify(onDone).invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -384,12 +619,5 @@ 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")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 528 KiB |
BIN
art/showcasemain4.png
Normal file
After Width: | Height: | Size: 512 KiB |
|
@ -15,7 +15,6 @@ 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +22,7 @@ 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" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,6 @@ 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
|
||||||
|
@ -34,7 +30,6 @@ 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
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
/**
|
|
||||||
* 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.utilities.ext
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
|
|
||||||
fun String.toUri() = Uri.parse(this)!!
|
|
||||||
|
|
||||||
fun String?.isNotNullOrEmpty(): Boolean {
|
|
||||||
if (this == null || this == "null") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return !isNullOrEmpty()
|
|
||||||
}
|
|
|
@ -21,12 +21,7 @@ import android.widget.EditText
|
||||||
import androidx.annotation.IntRange
|
import androidx.annotation.IntRange
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
fun EditText.setTextAndMaintainSelection(text: CharSequence?) {
|
fun EditText.setTextAndMaintainSelection(text: CharSequence) {
|
||||||
if (text == null) {
|
|
||||||
setText("")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val formerStart = min(selectionStart, text.length)
|
val formerStart = min(selectionStart, text.length)
|
||||||
val formerEnd = min(selectionEnd, text.length)
|
val formerEnd = min(selectionEnd, text.length)
|
||||||
setText(text)
|
setText(text)
|
||||||
|
|
|
@ -20,7 +20,6 @@ import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationCompat.BigTextStyle
|
|
||||||
import androidx.core.app.NotificationCompat.DEFAULT_VIBRATE
|
import androidx.core.app.NotificationCompat.DEFAULT_VIBRATE
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
|
@ -57,10 +56,6 @@ class RealNotificationProvider(
|
||||||
.setLargeIcon(largeIcon)
|
.setLargeIcon(largeIcon)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setDefaults(DEFAULT_VIBRATE)
|
.setDefaults(DEFAULT_VIBRATE)
|
||||||
.setStyle(
|
|
||||||
BigTextStyle()
|
|
||||||
.bigText(content)
|
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,6 @@ android {
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
|
||||||
exclude 'META-INF/atomicfu.kotlin_module'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
|
@ -181,8 +181,7 @@ 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)
|
||||||
|
@ -200,8 +199,7 @@ class AppDatabaseTest() {
|
||||||
validationMode = STATUS_CODE,
|
validationMode = STATUS_CODE,
|
||||||
validationArgs = null,
|
validationArgs = null,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
networkTimeout = 10000,
|
networkTimeout = 10000
|
||||||
certificate = null
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -229,8 +227,7 @@ class AppDatabaseTest() {
|
||||||
validationMode = STATUS_CODE,
|
validationMode = STATUS_CODE,
|
||||||
validationArgs = null,
|
validationArgs = null,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
networkTimeout = 10000,
|
networkTimeout = 10000
|
||||||
certificate = null
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -431,30 +428,9 @@ class AppDatabaseTest() {
|
||||||
|
|
||||||
val allSites = db.allSites()
|
val allSites = db.allSites()
|
||||||
assertThat(allSites.size).isEqualTo(3)
|
assertThat(allSites.size).isEqualTo(3)
|
||||||
assertThat(allSites[0]).isEqualTo(
|
assertThat(allSites[0]).isEqualTo(MOCK_MODEL_1)
|
||||||
MOCK_MODEL_1.copy(
|
assertThat(allSites[1]).isEqualTo(MOCK_MODEL_2)
|
||||||
headers = listOf(
|
assertThat(allSites[2]).isEqualTo(MOCK_MODEL_3)
|
||||||
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() {
|
||||||
|
@ -491,12 +467,10 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -35,8 +35,7 @@ fun fakeSettingsModel(
|
||||||
validationMode = validationMode,
|
validationMode = validationMode,
|
||||||
validationArgs = null,
|
validationArgs = null,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
networkTimeout = 10000,
|
networkTimeout = 10000
|
||||||
certificate = null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fun fakeResultModel(
|
fun fakeResultModel(
|
||||||
|
|
|
@ -34,7 +34,7 @@ import com.afollestad.nocknock.data.model.ValidationResult
|
||||||
SiteSettings::class,
|
SiteSettings::class,
|
||||||
Site::class
|
Site::class
|
||||||
],
|
],
|
||||||
version = 5,
|
version = 4,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|
|
@ -57,15 +57,3 @@ class Database3to4Migration : Migration(3, 4) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrates the database from version 4 to 5.
|
|
||||||
*
|
|
||||||
* @author Aidan Follestad (@afollestad)
|
|
||||||
*/
|
|
||||||
class Database4to5Migration : Migration(4, 5) {
|
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
|
||||||
database.execSQL("ALTER TABLE `site_settings` ADD COLUMN certificate TEXT")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -40,10 +40,8 @@ data class SiteSettings(
|
||||||
/** Whether or not the [Site] is enabled for automatic periodic checks. */
|
/** Whether or not the [Site] is enabled for automatic periodic checks. */
|
||||||
var disabled: Boolean,
|
var disabled: Boolean,
|
||||||
/** The network response timeout for validation attempts. */
|
/** The network response timeout for validation attempts. */
|
||||||
var networkTimeout: Int,
|
var networkTimeout: Int
|
||||||
/** The Uri to a self signed certificate. */
|
|
||||||
var certificate: String?
|
|
||||||
) : Serializable {
|
) : Serializable {
|
||||||
|
|
||||||
constructor() : this(0, 0, STATUS_CODE, null, false, 0, null)
|
constructor() : this(0, 0, STATUS_CODE, null, false, 0)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,56 +3,52 @@ ext.versions = [
|
||||||
minSdk : 21,
|
minSdk : 21,
|
||||||
compileSdk : 28,
|
compileSdk : 28,
|
||||||
buildTools : '28.0.3',
|
buildTools : '28.0.3',
|
||||||
publishVersion : '0.8.8',
|
publishVersion : '0.8.4',
|
||||||
publishVersionCode : 46,
|
publishVersionCode : 37,
|
||||||
|
|
||||||
// Plugins
|
// Plugins
|
||||||
gradlePlugin : '3.4.0',
|
gradlePlugin : '3.2.1',
|
||||||
spotlessPlugin : '3.22.0',
|
spotlessPlugin : '3.17.0',
|
||||||
versionPlugin : '0.21.0',
|
versionPlugin : '0.20.0',
|
||||||
googleServices : '4.2.0',
|
|
||||||
fabricPlugin : '1.+',
|
fabricPlugin : '1.+',
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
okHttp : '3.14.1',
|
okHttp : '3.12.1',
|
||||||
rhino : '1.7.10',
|
rhino : '1.7.10',
|
||||||
|
|
||||||
// Kotlin
|
// Kotlin
|
||||||
kotlin : '1.3.30',
|
kotlin : '1.3.11',
|
||||||
coroutines : '1.2.0',
|
coroutines : '1.1.0',
|
||||||
koin : '1.0.2',
|
koin : '1.0.2',
|
||||||
|
|
||||||
// Google/AndroidX
|
// Google/AndroidX
|
||||||
androidxAnnotations : '1.0.2',
|
androidxAnnotations : '1.0.1',
|
||||||
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.8.1',
|
materialDialogs : '2.0.0-rc7',
|
||||||
rxkPrefs : '1.2.5',
|
rxkPrefs : '1.2.1',
|
||||||
vvalidator : '0.4.1',
|
|
||||||
|
|
||||||
// Debugging
|
// Debugging
|
||||||
timber : '4.7.1',
|
timber : '4.7.1',
|
||||||
fabric : '2.9.9@aar',
|
fabric : '2.9.8@aar',
|
||||||
|
|
||||||
// Unit testing
|
// Unit testing
|
||||||
junit : '4.12',
|
junit : '4.12',
|
||||||
mockito : '2.27.0',
|
mockito : '2.23.4',
|
||||||
mockitoKotlin : '2.1.0',
|
mockitoKotlin : '2.0.0-RC1',
|
||||||
truth : '0.44',
|
truth : '0.42',
|
||||||
|
|
||||||
// UI testing
|
// UI testing
|
||||||
androidxTestRunner : '1.1.1',
|
androidxTestRunner : '1.1.1',
|
||||||
androidxTest : '1.1.0',
|
androidxTest : '1.1.0',
|
||||||
archTesting : '2.0.1'
|
archTesting : '2.0.0'
|
||||||
]
|
]
|
||||||
|
|
|
@ -15,8 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.engine
|
package com.afollestad.nocknock.engine
|
||||||
|
|
||||||
import com.afollestad.nocknock.engine.ssl.RealSslManager
|
|
||||||
import com.afollestad.nocknock.engine.ssl.SslManager
|
|
||||||
import com.afollestad.nocknock.engine.validation.RealValidationExecutor
|
import com.afollestad.nocknock.engine.validation.RealValidationExecutor
|
||||||
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||||
import org.koin.dsl.module.module
|
import org.koin.dsl.module.module
|
||||||
|
@ -25,8 +23,6 @@ import org.koin.dsl.module.module
|
||||||
val engineModule = module {
|
val engineModule = module {
|
||||||
|
|
||||||
single {
|
single {
|
||||||
RealValidationExecutor(get(), get(), get(), get(), get(), get(), get())
|
RealValidationExecutor(get(), get(), get(), get(), get(), get())
|
||||||
} bind ValidationExecutor::class
|
} bind ValidationExecutor::class
|
||||||
|
|
||||||
factory { RealSslManager(get()) } bind SslManager::class
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,98 +0,0 @@
|
||||||
/**
|
|
||||||
* 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.engine.ssl
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.annotation.CheckResult
|
|
||||||
import com.afollestad.nocknock.utilities.ext.toUri
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import java.io.BufferedInputStream
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.security.KeyStore
|
|
||||||
import java.security.cert.CertificateFactory
|
|
||||||
import javax.net.ssl.SSLContext
|
|
||||||
import javax.net.ssl.TrustManagerFactory
|
|
||||||
import javax.net.ssl.X509TrustManager
|
|
||||||
import timber.log.Timber.d as log
|
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
|
||||||
interface SslManager {
|
|
||||||
|
|
||||||
@CheckResult fun clientForCertificate(
|
|
||||||
certUri: String,
|
|
||||||
siteUri: String,
|
|
||||||
client: OkHttpClient
|
|
||||||
): OkHttpClient
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) **/
|
|
||||||
class RealSslManager(
|
|
||||||
private val app: Application
|
|
||||||
) : SslManager {
|
|
||||||
|
|
||||||
override fun clientForCertificate(
|
|
||||||
certUri: String,
|
|
||||||
siteUri: String,
|
|
||||||
client: OkHttpClient
|
|
||||||
): OkHttpClient {
|
|
||||||
val parsedCertUri = certUri.toUri()
|
|
||||||
val parsedSiteUri = siteUri.toUri()
|
|
||||||
val siteHost = parsedSiteUri.host ?: ""
|
|
||||||
|
|
||||||
log("Loading certificate $certUri for host $siteHost")
|
|
||||||
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
|
|
||||||
keyStore.load(null, null)
|
|
||||||
|
|
||||||
val certInputStream = app.openUri(parsedCertUri)
|
|
||||||
val bis = BufferedInputStream(certInputStream)
|
|
||||||
val certificateFactory = CertificateFactory.getInstance("X.509")
|
|
||||||
|
|
||||||
while (bis.available() > 0) {
|
|
||||||
val cert = certificateFactory.generateCertificate(bis)
|
|
||||||
keyStore.setCertificateEntry(siteHost, cert)
|
|
||||||
}
|
|
||||||
|
|
||||||
val trustManagerFactory =
|
|
||||||
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
|
||||||
trustManagerFactory.init(keyStore)
|
|
||||||
|
|
||||||
val trustManagers = trustManagerFactory.trustManagers
|
|
||||||
val sslContext = SSLContext.getInstance("TLS")
|
|
||||||
sslContext.init(null, trustManagers, null)
|
|
||||||
|
|
||||||
val trustManager = trustManagers.first() as X509TrustManager
|
|
||||||
log("Loaded successfully!")
|
|
||||||
return client.newBuilder()
|
|
||||||
.sslSocketFactory(sslContext.socketFactory, trustManager)
|
|
||||||
.hostnameVerifier { hostname, _ ->
|
|
||||||
log("Verifying hostname $hostname")
|
|
||||||
hostname == siteHost
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Context.openUri(uri: Uri) = when (uri.scheme) {
|
|
||||||
"content" -> {
|
|
||||||
contentResolver.openInputStream(uri) ?: throw IllegalStateException(
|
|
||||||
"Unable to open input stream to $uri"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
"file" -> FileInputStream(uri.path)
|
|
||||||
else -> FileInputStream(uri.toString())
|
|
||||||
}
|
|
|
@ -17,26 +17,21 @@ package com.afollestad.nocknock.engine.validation
|
||||||
|
|
||||||
import android.app.job.JobScheduler
|
import android.app.job.JobScheduler
|
||||||
import android.app.job.JobScheduler.RESULT_SUCCESS
|
import android.app.job.JobScheduler.RESULT_SUCCESS
|
||||||
import android.net.Uri
|
|
||||||
import com.afollestad.nocknock.data.AppDatabase
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.data.allSites
|
import com.afollestad.nocknock.data.allSites
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.Status.ERROR
|
import com.afollestad.nocknock.data.model.Status.ERROR
|
||||||
import com.afollestad.nocknock.data.model.Status.OK
|
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.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
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
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) */
|
||||||
|
@ -47,8 +42,6 @@ data class CheckResult(
|
||||||
|
|
||||||
typealias ClientTimeoutChanger = (client: OkHttpClient, timeout: Int) -> OkHttpClient
|
typealias ClientTimeoutChanger = (client: OkHttpClient, timeout: Int) -> OkHttpClient
|
||||||
|
|
||||||
typealias UriConverter = (String) -> Uri
|
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
interface ValidationExecutor {
|
interface ValidationExecutor {
|
||||||
|
|
||||||
|
@ -73,8 +66,7 @@ class RealValidationExecutor(
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val bundleProvider: BundleProvider,
|
private val bundleProvider: BundleProvider,
|
||||||
private val jobInfoProvider: JobInfoProvider,
|
private val jobInfoProvider: JobInfoProvider,
|
||||||
private val database: AppDatabase,
|
private val database: AppDatabase
|
||||||
private val sslManager: SslManager
|
|
||||||
) : ValidationExecutor {
|
) : ValidationExecutor {
|
||||||
|
|
||||||
private var clientTimeoutChanger: ClientTimeoutChanger = { client, timeout ->
|
private var clientTimeoutChanger: ClientTimeoutChanger = { client, timeout ->
|
||||||
|
@ -155,32 +147,21 @@ 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
|
site.headers.forEach { header ->
|
||||||
.filter { header -> header.key.isNotNullOrEmpty() }
|
|
||||||
.forEach { header ->
|
|
||||||
addHeader(header.key, header.value)
|
addHeader(header.key, header.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val timeout = max(siteSettings.networkTimeout, 1)
|
val client = clientTimeoutChanger(okHttpClient, siteSettings.networkTimeout)
|
||||||
val clientWithTimeout = clientTimeoutChanger(okHttpClient, timeout)
|
|
||||||
val client = if (siteSettings.certificate.isNotNullOrEmpty()) {
|
|
||||||
sslManager.clientForCertificate(
|
|
||||||
certUri = siteSettings.certificate!!,
|
|
||||||
siteUri = site.url,
|
|
||||||
client = clientWithTimeout
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
clientWithTimeout
|
|
||||||
}
|
|
||||||
val response = client.newCall(request)
|
val response = client.newCall(request)
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
|
@ -209,7 +190,6 @@ 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))
|
||||||
}
|
}
|
||||||
|
@ -219,7 +199,7 @@ class RealValidationExecutor(
|
||||||
jobScheduler.allPendingJobs
|
jobScheduler.allPendingJobs
|
||||||
.firstOrNull { job -> job.id == site.id.toInt() }
|
.firstOrNull { job -> job.id == site.id.toInt() }
|
||||||
|
|
||||||
@TestOnly fun setClientTimeoutChanger(changer: ClientTimeoutChanger) {
|
// @TestOnly fun setClientTimeoutChanger(changer: ClientTimeoutChanger) {
|
||||||
this.clientTimeoutChanger = changer
|
// this.clientTimeoutChanger = changer
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,189 +0,0 @@
|
||||||
/**
|
|
||||||
* 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.engine
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import com.afollestad.nocknock.data.AppDatabase
|
|
||||||
import com.afollestad.nocknock.data.HeaderDao
|
|
||||||
import com.afollestad.nocknock.data.RetryPolicyDao
|
|
||||||
import com.afollestad.nocknock.data.SiteDao
|
|
||||||
import com.afollestad.nocknock.data.SiteSettingsDao
|
|
||||||
import com.afollestad.nocknock.data.ValidationResultsDao
|
|
||||||
import com.afollestad.nocknock.data.model.Header
|
|
||||||
import com.afollestad.nocknock.data.model.RetryPolicy
|
|
||||||
import com.afollestad.nocknock.data.model.Site
|
|
||||||
import com.afollestad.nocknock.data.model.SiteSettings
|
|
||||||
import com.afollestad.nocknock.data.model.Status
|
|
||||||
import com.afollestad.nocknock.data.model.Status.OK
|
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode
|
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
|
||||||
import com.afollestad.nocknock.data.model.ValidationResult
|
|
||||||
import com.nhaarman.mockitokotlin2.doAnswer
|
|
||||||
import com.nhaarman.mockitokotlin2.doReturn
|
|
||||||
import com.nhaarman.mockitokotlin2.isA
|
|
||||||
import com.nhaarman.mockitokotlin2.mock
|
|
||||||
import java.lang.System.currentTimeMillis
|
|
||||||
|
|
||||||
fun fakeIntent(action: String): Intent {
|
|
||||||
return mock {
|
|
||||||
on { getAction() } doReturn action
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fakeSettingsModel(
|
|
||||||
id: Long,
|
|
||||||
validationMode: ValidationMode = STATUS_CODE
|
|
||||||
) = SiteSettings(
|
|
||||||
siteId = id,
|
|
||||||
validationIntervalMs = 600000,
|
|
||||||
validationMode = validationMode,
|
|
||||||
validationArgs = null,
|
|
||||||
disabled = false,
|
|
||||||
networkTimeout = 10000,
|
|
||||||
certificate = null
|
|
||||||
)
|
|
||||||
|
|
||||||
fun fakeResultModel(
|
|
||||||
id: Long,
|
|
||||||
status: Status = OK,
|
|
||||||
reason: String? = null
|
|
||||||
) = ValidationResult(
|
|
||||||
siteId = id,
|
|
||||||
status = status,
|
|
||||||
reason = reason,
|
|
||||||
timestampMs = currentTimeMillis()
|
|
||||||
)
|
|
||||||
|
|
||||||
fun fakeRetryPolicy(
|
|
||||||
id: Long,
|
|
||||||
count: Int = 3,
|
|
||||||
minutes: Int = 6
|
|
||||||
) = RetryPolicy(
|
|
||||||
siteId = id,
|
|
||||||
count = count,
|
|
||||||
minutes = minutes
|
|
||||||
)
|
|
||||||
|
|
||||||
fun fakeHeaders(siteId: Long): List<Header> {
|
|
||||||
return listOf(
|
|
||||||
Header(id = siteId + 1, siteId = siteId, key = "Content-Type", value = "text/html"),
|
|
||||||
Header(id = siteId + 2, siteId = siteId, key = "User-Agent", value = "NockNock")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fakeModel(id: Long) = Site(
|
|
||||||
id = id,
|
|
||||||
name = "Test",
|
|
||||||
url = "https://test.com",
|
|
||||||
tags = "",
|
|
||||||
settings = fakeSettingsModel(id),
|
|
||||||
lastResult = fakeResultModel(id),
|
|
||||||
retryPolicy = fakeRetryPolicy(id),
|
|
||||||
headers = fakeHeaders(id)
|
|
||||||
)
|
|
||||||
|
|
||||||
val MOCK_MODEL_1 = fakeModel(1)
|
|
||||||
val MOCK_MODEL_2 = fakeModel(2)
|
|
||||||
val MOCK_MODEL_3 = fakeModel(3)
|
|
||||||
val ALL_MOCK_MODELS = listOf(MOCK_MODEL_1, MOCK_MODEL_2, MOCK_MODEL_3)
|
|
||||||
|
|
||||||
fun mockDatabase(): AppDatabase {
|
|
||||||
val siteDao = mock<SiteDao> {
|
|
||||||
on { insert(isA()) } doReturn 1
|
|
||||||
on { one(isA()) } doAnswer { inv ->
|
|
||||||
val id = inv.getArgument<Long>(0)
|
|
||||||
return@doAnswer when (id) {
|
|
||||||
1L -> listOf(MOCK_MODEL_1)
|
|
||||||
2L -> listOf(MOCK_MODEL_2)
|
|
||||||
3L -> listOf(MOCK_MODEL_3)
|
|
||||||
else -> listOf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
on { all() } doReturn ALL_MOCK_MODELS
|
|
||||||
on { update(isA()) } doAnswer { inv ->
|
|
||||||
return@doAnswer inv.arguments.size
|
|
||||||
}
|
|
||||||
on { delete(isA()) } doAnswer { inv ->
|
|
||||||
return@doAnswer inv.arguments.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val settingsDao = mock<SiteSettingsDao> {
|
|
||||||
on { insert(isA()) } doReturn 1L
|
|
||||||
on { forSite(isA()) } doAnswer { inv ->
|
|
||||||
val id = inv.getArgument<Long>(0)
|
|
||||||
return@doAnswer when (id) {
|
|
||||||
1L -> listOf(MOCK_MODEL_1.settings!!)
|
|
||||||
2L -> listOf(MOCK_MODEL_2.settings!!)
|
|
||||||
3L -> listOf(MOCK_MODEL_3.settings!!)
|
|
||||||
else -> listOf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
on { update(isA()) } doReturn 1
|
|
||||||
on { delete(isA()) } doReturn 1
|
|
||||||
}
|
|
||||||
val resultsDao = mock<ValidationResultsDao> {
|
|
||||||
on { insert(isA()) } doReturn 1L
|
|
||||||
on { forSite(isA()) } doAnswer { inv ->
|
|
||||||
val id = inv.getArgument<Long>(0)
|
|
||||||
return@doAnswer when (id) {
|
|
||||||
1L -> listOf(MOCK_MODEL_1.lastResult!!)
|
|
||||||
2L -> listOf(MOCK_MODEL_2.lastResult!!)
|
|
||||||
3L -> listOf(MOCK_MODEL_3.lastResult!!)
|
|
||||||
else -> listOf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
on { update(isA()) } doReturn 1
|
|
||||||
on { delete(isA()) } doReturn 1
|
|
||||||
}
|
|
||||||
val retryDao = mock<RetryPolicyDao> {
|
|
||||||
on { insert(isA()) } doReturn 1L
|
|
||||||
on { forSite(isA()) } doAnswer { inv ->
|
|
||||||
val id = inv.getArgument<Long>(0)
|
|
||||||
return@doAnswer when (id) {
|
|
||||||
1L -> listOf(MOCK_MODEL_1.retryPolicy!!)
|
|
||||||
2L -> listOf(MOCK_MODEL_2.retryPolicy!!)
|
|
||||||
3L -> listOf(MOCK_MODEL_3.retryPolicy!!)
|
|
||||||
else -> listOf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
on { update(isA()) } doReturn 1
|
|
||||||
on { delete(isA()) } doReturn 1
|
|
||||||
}
|
|
||||||
val headerDao = mock<HeaderDao> {
|
|
||||||
on { all() } doReturn MOCK_MODEL_1.headers + MOCK_MODEL_2.headers + MOCK_MODEL_3.headers
|
|
||||||
on { forSite(isA()) } doAnswer { inv ->
|
|
||||||
val id = inv.getArgument<Long>(0)
|
|
||||||
return@doAnswer when (id) {
|
|
||||||
1L -> MOCK_MODEL_1.headers
|
|
||||||
2L -> MOCK_MODEL_2.headers
|
|
||||||
3L -> MOCK_MODEL_3.headers
|
|
||||||
else -> listOf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
on { insert(isA<Header>()) } doReturn 1L
|
|
||||||
on { insert(isA<List<Header>>()) } doReturn listOf(1L, 2L)
|
|
||||||
on { update(isA()) } doReturn 1
|
|
||||||
on { delete(isA()) } doReturn 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return mock {
|
|
||||||
on { siteDao() } doReturn siteDao
|
|
||||||
on { siteSettingsDao() } doReturn settingsDao
|
|
||||||
on { validationResultsDao() } doReturn resultsDao
|
|
||||||
on { retryPolicyDao() } doReturn retryDao
|
|
||||||
on { headerDao() } doReturn headerDao
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,12 +17,13 @@ package com.afollestad.nocknock.engine
|
||||||
|
|
||||||
import android.app.job.JobInfo
|
import android.app.job.JobInfo
|
||||||
import android.app.job.JobScheduler
|
import android.app.job.JobScheduler
|
||||||
import com.afollestad.nocknock.data.model.Header
|
import com.afollestad.nocknock.data.legacy.ServerModel
|
||||||
import com.afollestad.nocknock.data.model.Status.ERROR
|
import com.afollestad.nocknock.data.model.Status.ERROR
|
||||||
import com.afollestad.nocknock.data.model.Status.OK
|
import com.afollestad.nocknock.data.model.Status.OK
|
||||||
import com.afollestad.nocknock.engine.ssl.SslManager
|
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||||
import com.afollestad.nocknock.engine.validation.RealValidationExecutor
|
import com.afollestad.nocknock.data.legacy.ServerModelStore
|
||||||
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.engine.validation.RealValidationManager
|
||||||
import com.afollestad.nocknock.utilities.providers.StringProvider
|
import com.afollestad.nocknock.utilities.providers.StringProvider
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import com.nhaarman.mockitokotlin2.any
|
import com.nhaarman.mockitokotlin2.any
|
||||||
|
@ -44,7 +45,7 @@ import okhttp3.ResponseBody
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
|
|
||||||
class ValidationExecutorTest {
|
class CheckStatusManagerTest {
|
||||||
|
|
||||||
private val timeoutError = "Oh no, a timeout"
|
private val timeoutError = "Oh no, a timeout"
|
||||||
|
|
||||||
|
@ -55,21 +56,15 @@ class ValidationExecutorTest {
|
||||||
}
|
}
|
||||||
private val bundleProvider = testBundleProvider()
|
private val bundleProvider = testBundleProvider()
|
||||||
private val jobInfoProvider = testJobInfoProvider()
|
private val jobInfoProvider = testJobInfoProvider()
|
||||||
private val database = mockDatabase()
|
private val store = mock<ServerModelStore>()
|
||||||
private val sslManager = mock<SslManager> {
|
|
||||||
on { clientForCertificate(any(), any(), any()) } doAnswer { inv ->
|
|
||||||
inv.getArgument<OkHttpClient>(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val manager = RealValidationExecutor(
|
private val manager = RealValidationManager(
|
||||||
jobScheduler,
|
jobScheduler,
|
||||||
okHttpClient,
|
okHttpClient,
|
||||||
stringProvider,
|
stringProvider,
|
||||||
bundleProvider,
|
bundleProvider,
|
||||||
jobInfoProvider,
|
jobInfoProvider,
|
||||||
database,
|
store
|
||||||
sslManager
|
|
||||||
).apply {
|
).apply {
|
||||||
setClientTimeoutChanger { _, timeout ->
|
setClientTimeoutChanger { _, timeout ->
|
||||||
whenever(okHttpClient.callTimeoutMillis()).doReturn(timeout)
|
whenever(okHttpClient.callTimeoutMillis()).doReturn(timeout)
|
||||||
|
@ -77,241 +72,202 @@ class ValidationExecutorTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun ensureScheduledValidations_noEnabledSites() = runBlocking {
|
@Test fun ensureScheduledChecks_noEnabledSites() = runBlocking {
|
||||||
val model1 = fakeModel(id = 1)
|
val model1 = fakeModel().copy(disabled = true)
|
||||||
model1.settings = model1.settings!!.copy(disabled = true)
|
whenever(store.get()).doReturn(listOf(model1))
|
||||||
database.setAllSites(model1)
|
|
||||||
|
|
||||||
manager.ensureScheduledValidations()
|
manager.ensureScheduledChecks()
|
||||||
|
|
||||||
verifyNoMoreInteractions(jobScheduler)
|
verifyNoMoreInteractions(jobScheduler)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun ensureScheduledValidations_sitesAlreadyHaveJobs() = runBlocking<Unit> {
|
@Test fun ensureScheduledChecks_sitesAlreadyHaveJobs() = runBlocking<Unit> {
|
||||||
val model1 = fakeModel(id = 1)
|
val model1 = fakeModel()
|
||||||
val job1 = fakeJob(1)
|
val job1 = fakeJob(model1.id)
|
||||||
database.setAllSites(model1)
|
whenever(store.get()).doReturn(listOf(model1))
|
||||||
whenever(jobScheduler.allPendingJobs).doReturn(listOf(job1))
|
whenever(jobScheduler.allPendingJobs).doReturn(listOf(job1))
|
||||||
|
|
||||||
manager.ensureScheduledValidations()
|
manager.ensureScheduledChecks()
|
||||||
|
|
||||||
verify(jobScheduler, never()).schedule(any())
|
verify(jobScheduler, never()).schedule(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun ensureScheduledValidations() = runBlocking {
|
@Test fun ensureScheduledChecks() = runBlocking {
|
||||||
val model1 = fakeModel(id = 1)
|
val model1 = fakeModel()
|
||||||
database.setAllSites(model1)
|
whenever(store.get()).doReturn(listOf(model1))
|
||||||
|
|
||||||
whenever(jobScheduler.allPendingJobs).doReturn(listOf<JobInfo>())
|
whenever(jobScheduler.allPendingJobs).doReturn(listOf<JobInfo>())
|
||||||
|
|
||||||
manager.ensureScheduledValidations()
|
manager.ensureScheduledChecks()
|
||||||
|
|
||||||
val jobCaptor = argumentCaptor<JobInfo>()
|
val jobCaptor = argumentCaptor<JobInfo>()
|
||||||
verify(jobScheduler).schedule(jobCaptor.capture())
|
verify(jobScheduler).schedule(jobCaptor.capture())
|
||||||
val jobInfo = jobCaptor.allValues.single()
|
val jobInfo = jobCaptor.allValues.single()
|
||||||
assertThat(jobInfo.id).isEqualTo(model1.id)
|
assertThat(jobInfo.id).isEqualTo(model1.id)
|
||||||
assertThat(jobInfo.extras.getLong(KEY_SITE_ID)).isEqualTo(model1.id)
|
assertThat(jobInfo.extras.getInt(KEY_SITE_ID)).isEqualTo(model1.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun scheduleValidation_rightNow() {
|
@Test fun scheduleCheck_rightNow() {
|
||||||
val model1 = fakeModel(id = 1)
|
val model1 = fakeModel()
|
||||||
whenever(jobScheduler.allPendingJobs).doReturn(listOf<JobInfo>())
|
whenever(jobScheduler.allPendingJobs).doReturn(listOf<JobInfo>())
|
||||||
|
|
||||||
manager.scheduleValidation(
|
manager.scheduleCheck(
|
||||||
site = model1,
|
site = model1,
|
||||||
rightNow = true
|
rightNow = true
|
||||||
)
|
)
|
||||||
|
|
||||||
val jobCaptor = argumentCaptor<JobInfo>()
|
val jobCaptor = argumentCaptor<JobInfo>()
|
||||||
verify(jobScheduler).schedule(jobCaptor.capture())
|
verify(jobScheduler).schedule(jobCaptor.capture())
|
||||||
verify(jobScheduler).cancel(1)
|
verify(jobScheduler).cancel(model1.id)
|
||||||
|
|
||||||
val jobInfo = jobCaptor.allValues.single()
|
val jobInfo = jobCaptor.allValues.single()
|
||||||
assertThat(jobInfo.id).isEqualTo(model1.id)
|
assertThat(jobInfo.id).isEqualTo(model1.id)
|
||||||
assertThat(jobInfo.extras.getLong(KEY_SITE_ID)).isEqualTo(model1.id)
|
assertThat(jobInfo.extras.getInt(KEY_SITE_ID)).isEqualTo(model1.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = IllegalStateException::class)
|
@Test(expected = IllegalStateException::class)
|
||||||
fun scheduleValidation_notFromFinishingJob_haveExistingJob() {
|
fun scheduleCheck_notFromFinishingJob_haveExistingJob() {
|
||||||
val model1 = fakeModel(id = 1)
|
val model1 = fakeModel()
|
||||||
val job1 = fakeJob(1)
|
val job1 = fakeJob(model1.id)
|
||||||
whenever(jobScheduler.allPendingJobs).doReturn(listOf(job1))
|
whenever(jobScheduler.allPendingJobs).doReturn(listOf(job1))
|
||||||
|
|
||||||
manager.scheduleValidation(
|
manager.scheduleCheck(
|
||||||
site = model1,
|
site = model1,
|
||||||
fromFinishingJob = false
|
fromFinishingJob = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun scheduleValidation_fromFinishingJob_haveExistingJob() {
|
@Test fun scheduleCheck_fromFinishingJob_haveExistingJob() {
|
||||||
val model1 = fakeModel(id = 1)
|
val model1 = fakeModel()
|
||||||
val job1 = fakeJob(1)
|
val job1 = fakeJob(model1.id)
|
||||||
whenever(jobScheduler.allPendingJobs).doReturn(listOf(job1))
|
whenever(jobScheduler.allPendingJobs).doReturn(listOf(job1))
|
||||||
|
|
||||||
manager.scheduleValidation(
|
manager.scheduleCheck(
|
||||||
site = model1,
|
site = model1,
|
||||||
fromFinishingJob = true
|
fromFinishingJob = true
|
||||||
)
|
)
|
||||||
|
|
||||||
val jobCaptor = argumentCaptor<JobInfo>()
|
val jobCaptor = argumentCaptor<JobInfo>()
|
||||||
verify(jobScheduler).schedule(jobCaptor.capture())
|
verify(jobScheduler).schedule(jobCaptor.capture())
|
||||||
verify(jobScheduler, never()).cancel(any())
|
verify(jobScheduler, never()).cancel(model1.id)
|
||||||
|
|
||||||
val jobInfo = jobCaptor.allValues.single()
|
val jobInfo = jobCaptor.allValues.single()
|
||||||
assertThat(jobInfo.id).isEqualTo(model1.id)
|
assertThat(jobInfo.id).isEqualTo(model1.id)
|
||||||
assertThat(jobInfo.extras.getLong(KEY_SITE_ID)).isEqualTo(model1.id)
|
assertThat(jobInfo.extras.getInt(KEY_SITE_ID)).isEqualTo(model1.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun scheduleValidation() {
|
@Test fun scheduleCheck() {
|
||||||
val model1 = fakeModel(id = 1)
|
val model1 = fakeModel()
|
||||||
whenever(jobScheduler.allPendingJobs).doReturn(listOf<JobInfo>())
|
whenever(jobScheduler.allPendingJobs).doReturn(listOf<JobInfo>())
|
||||||
|
|
||||||
manager.scheduleValidation(
|
manager.scheduleCheck(
|
||||||
site = model1,
|
site = model1,
|
||||||
fromFinishingJob = true
|
fromFinishingJob = true
|
||||||
)
|
)
|
||||||
|
|
||||||
val jobCaptor = argumentCaptor<JobInfo>()
|
val jobCaptor = argumentCaptor<JobInfo>()
|
||||||
verify(jobScheduler).schedule(jobCaptor.capture())
|
verify(jobScheduler).schedule(jobCaptor.capture())
|
||||||
verify(jobScheduler, never()).cancel(any())
|
verify(jobScheduler, never()).cancel(model1.id)
|
||||||
|
|
||||||
val jobInfo = jobCaptor.allValues.single()
|
val jobInfo = jobCaptor.allValues.single()
|
||||||
assertThat(jobInfo.id).isEqualTo(model1.id)
|
assertThat(jobInfo.id).isEqualTo(model1.id)
|
||||||
assertThat(jobInfo.extras.getLong(KEY_SITE_ID)).isEqualTo(model1.id)
|
assertThat(jobInfo.extras.getInt(KEY_SITE_ID)).isEqualTo(model1.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun cancelScheduledValidation() {
|
@Test fun cancelCheck() {
|
||||||
val model1 = fakeModel(id = 1)
|
val model1 = fakeModel()
|
||||||
manager.cancelScheduledValidation(model1)
|
manager.cancelCheck(model1)
|
||||||
verify(jobScheduler).cancel(1)
|
verify(jobScheduler).cancel(model1.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun performValidation_httpNotSuccess() = runBlocking {
|
@Test fun performCheck_httpNotSuccess() = runBlocking {
|
||||||
val response = fakeResponse(500, "Internal Server Error", "Hello World")
|
val response = fakeResponse(500, "Internal Server Error", "Hello World")
|
||||||
val call = mock<Call> {
|
val call = mock<Call> {
|
||||||
on { execute() } doReturn response
|
on { execute() } doReturn response
|
||||||
}
|
}
|
||||||
whenever(okHttpClient.newCall(any())).doReturn(call)
|
whenever(okHttpClient.newCall(any())).doReturn(call)
|
||||||
|
|
||||||
val model1 = fakeModel(id = 1)
|
val model1 = fakeModel()
|
||||||
val result = manager.performValidation(model1)
|
val result = manager.performCheck(model1)
|
||||||
|
|
||||||
assertThat(result.model).isEqualTo(
|
assertThat(result.model).isEqualTo(
|
||||||
model1.copy(
|
model1.copy(
|
||||||
lastResult = model1.lastResult?.copy(
|
|
||||||
status = ERROR,
|
status = ERROR,
|
||||||
reason = "Response 500 - Hello World"
|
reason = "Response 500 - Hello World"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun performValidation_socketTimeout() = runBlocking {
|
@Test fun performCheck_socketTimeout() = runBlocking {
|
||||||
val error = SocketTimeoutException("Oh no!")
|
val error = SocketTimeoutException("Oh no!")
|
||||||
val call = mock<Call> {
|
val call = mock<Call> {
|
||||||
on { execute() } doAnswer { throw error }
|
on { execute() } doAnswer { throw error }
|
||||||
}
|
}
|
||||||
whenever(okHttpClient.newCall(any())).doReturn(call)
|
whenever(okHttpClient.newCall(any())).doReturn(call)
|
||||||
|
|
||||||
val model1 = fakeModel(id = 1)
|
val model1 = fakeModel()
|
||||||
val result = manager.performValidation(model1)
|
val result = manager.performCheck(model1)
|
||||||
|
|
||||||
assertThat(result.model).isEqualTo(
|
assertThat(result.model).isEqualTo(
|
||||||
model1.copy(
|
model1.copy(
|
||||||
lastResult = model1.lastResult?.copy(
|
|
||||||
status = ERROR,
|
status = ERROR,
|
||||||
reason = timeoutError
|
reason = timeoutError
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun performValidation_exception() = runBlocking {
|
@Test fun performCheck_exception() = runBlocking {
|
||||||
val error = Exception("Oh no!")
|
val error = Exception("Oh no!")
|
||||||
val call = mock<Call> {
|
val call = mock<Call> {
|
||||||
on { execute() } doAnswer { throw error }
|
on { execute() } doAnswer { throw error }
|
||||||
}
|
}
|
||||||
whenever(okHttpClient.newCall(any())).doReturn(call)
|
whenever(okHttpClient.newCall(any())).doReturn(call)
|
||||||
|
|
||||||
val model1 = fakeModel(id = 1)
|
val model1 = fakeModel()
|
||||||
val result = manager.performValidation(model1)
|
val result = manager.performCheck(model1)
|
||||||
|
|
||||||
assertThat(result.model).isEqualTo(
|
assertThat(result.model).isEqualTo(
|
||||||
model1.copy(
|
model1.copy(
|
||||||
lastResult = model1.lastResult?.copy(
|
|
||||||
status = ERROR,
|
status = ERROR,
|
||||||
reason = "Oh no!"
|
reason = "Oh no!"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun performValidation_success_withHeaders() = runBlocking {
|
@Test fun performCheck_success() = runBlocking {
|
||||||
val requestCaptor = argumentCaptor<Request>()
|
|
||||||
val response = fakeResponse(200, "OK", "Hello World")
|
|
||||||
|
|
||||||
val call = mock<Call> {
|
|
||||||
on { execute() } doReturn response
|
|
||||||
}
|
|
||||||
whenever(okHttpClient.newCall(requestCaptor.capture()))
|
|
||||||
.doReturn(call)
|
|
||||||
|
|
||||||
val model1 = fakeModel(id = 1).copy(
|
|
||||||
headers = listOf(
|
|
||||||
Header(
|
|
||||||
key = "X-Test-Header",
|
|
||||||
value = "Hello, World!"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val result = manager.performValidation(model1)
|
|
||||||
val httpRequest = requestCaptor.firstValue
|
|
||||||
|
|
||||||
assertThat(result.model).isEqualTo(
|
|
||||||
model1.copy(
|
|
||||||
lastResult = model1.lastResult?.copy(
|
|
||||||
status = OK,
|
|
||||||
reason = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assertThat(okHttpClient.callTimeoutMillis())
|
|
||||||
.isEqualTo(model1.settings!!.networkTimeout)
|
|
||||||
assertThat(httpRequest.header("X-Test-Header"))
|
|
||||||
.isEqualTo("Hello, World!")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun performValidation_success_withCustomSslCert() = runBlocking<Unit> {
|
|
||||||
val response = fakeResponse(200, "OK", "Hello World")
|
val response = fakeResponse(200, "OK", "Hello World")
|
||||||
val call = mock<Call> {
|
val call = mock<Call> {
|
||||||
on { execute() } doReturn response
|
on { execute() } doReturn response
|
||||||
}
|
}
|
||||||
whenever(okHttpClient.newCall(any())).doReturn(call)
|
whenever(okHttpClient.newCall(any())).doReturn(call)
|
||||||
|
|
||||||
val model1 = fakeModel(id = 1).copy(
|
val model1 = fakeModel()
|
||||||
url = "http://wwww.mysite.com/test.html",
|
val result = manager.performCheck(model1)
|
||||||
headers = emptyList()
|
|
||||||
)
|
|
||||||
model1.settings = model1.settings!!.copy(
|
|
||||||
certificate = "file:///sdcard/cert.pem"
|
|
||||||
)
|
|
||||||
val result = manager.performValidation(model1)
|
|
||||||
|
|
||||||
assertThat(result.model).isEqualTo(
|
assertThat(result.model).isEqualTo(
|
||||||
model1.copy(
|
model1.copy(
|
||||||
lastResult = model1.lastResult?.copy(
|
|
||||||
status = OK,
|
status = OK,
|
||||||
reason = null
|
reason = null
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
assertThat(okHttpClient.callTimeoutMillis())
|
assertThat(okHttpClient.callTimeoutMillis())
|
||||||
.isEqualTo(model1.settings!!.networkTimeout)
|
.isEqualTo(model1.networkTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
verify(sslManager).clientForCertificate(
|
@Test fun performCheck_401_butStillSuccess() = runBlocking {
|
||||||
"file:///sdcard/cert.pem",
|
val response = fakeResponse(401, "Unauthorized", "Hello World")
|
||||||
"http://wwww.mysite.com/test.html",
|
val call = mock<Call> {
|
||||||
okHttpClient
|
on { execute() } doReturn response
|
||||||
|
}
|
||||||
|
whenever(okHttpClient.newCall(any())).doReturn(call)
|
||||||
|
|
||||||
|
val model1 = fakeModel()
|
||||||
|
val result = manager.performCheck(model1)
|
||||||
|
|
||||||
|
assertThat(result.model).isEqualTo(
|
||||||
|
model1.copy(
|
||||||
|
status = OK,
|
||||||
|
reason = null
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,6 +293,14 @@ class ValidationExecutorTest {
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun fakeModel() = ServerModel(
|
||||||
|
id = 1,
|
||||||
|
name = "Wakanda Forever",
|
||||||
|
url = "https://www.wakanda.gov",
|
||||||
|
validationMode = STATUS_CODE,
|
||||||
|
networkTimeout = 60000
|
||||||
|
)
|
||||||
|
|
||||||
private fun fakeJob(id: Int): JobInfo {
|
private fun fakeJob(id: Int): JobInfo {
|
||||||
return mock {
|
return mock {
|
||||||
on { this.id } doReturn id
|
on { this.id } doReturn id
|
|
@ -18,8 +18,6 @@ package com.afollestad.nocknock.engine
|
||||||
import android.app.job.JobInfo
|
import android.app.job.JobInfo
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.os.PersistableBundle
|
import android.os.PersistableBundle
|
||||||
import com.afollestad.nocknock.data.AppDatabase
|
|
||||||
import com.afollestad.nocknock.data.model.Site
|
|
||||||
import com.afollestad.nocknock.utilities.providers.BundleProvider
|
import com.afollestad.nocknock.utilities.providers.BundleProvider
|
||||||
import com.afollestad.nocknock.utilities.providers.IBundle
|
import com.afollestad.nocknock.utilities.providers.IBundle
|
||||||
import com.afollestad.nocknock.utilities.providers.IBundler
|
import com.afollestad.nocknock.utilities.providers.IBundler
|
||||||
|
@ -36,11 +34,11 @@ fun testBundleProvider(): BundleProvider {
|
||||||
val realBundle = mock<PersistableBundle>()
|
val realBundle = mock<PersistableBundle>()
|
||||||
val creator = it.getArgument<IBundler>(0)
|
val creator = it.getArgument<IBundler>(0)
|
||||||
creator(object : IBundle {
|
creator(object : IBundle {
|
||||||
override fun putLong(
|
override fun putInt(
|
||||||
key: String,
|
key: String,
|
||||||
value: Long
|
value: Int
|
||||||
) {
|
) {
|
||||||
whenever(realBundle.getLong(key)).doReturn(value)
|
whenever(realBundle.getInt(key)).doReturn(value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return@doAnswer realBundle
|
return@doAnswer realBundle
|
||||||
|
@ -68,21 +66,3 @@ fun testJobInfoProvider(): JobInfoProvider {
|
||||||
}
|
}
|
||||||
return provider
|
return provider
|
||||||
}
|
}
|
||||||
|
|
||||||
fun AppDatabase.setAllSites(vararg sites: Site) {
|
|
||||||
whenever(siteDao().all()).doReturn(listOf(*sites))
|
|
||||||
for (site in sites) {
|
|
||||||
whenever(siteSettingsDao().forSite(site.id))
|
|
||||||
.doReturn(listOf(site.settings!!))
|
|
||||||
if (site.lastResult != null) {
|
|
||||||
whenever(validationResultsDao().forSite(site.id))
|
|
||||||
.doReturn(listOf(site.lastResult!!))
|
|
||||||
}
|
|
||||||
if (site.retryPolicy != null) {
|
|
||||||
whenever(retryPolicyDao().forSite(site.id))
|
|
||||||
.doReturn(listOf(site.retryPolicy!!))
|
|
||||||
}
|
|
||||||
whenever(headerDao().forSite(site.id))
|
|
||||||
.doReturn(site.headers)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,6 +16,3 @@ 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
|
|
||||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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-5.1.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip
|
||||||
|
|
BIN
ic_web.png
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 25 KiB |
|
@ -1,42 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
// 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()
|
|
||||||
}
|
|
|
@ -19,8 +19,7 @@ import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import com.afollestad.nocknock.notifications.Channel.ValidationErrors
|
import com.afollestad.nocknock.notifications.Channel.CheckFailures
|
||||||
import com.afollestad.nocknock.notifications.Channel.ValidationSuccess
|
|
||||||
import com.afollestad.nocknock.utilities.providers.CanNotifyModel
|
import com.afollestad.nocknock.utilities.providers.CanNotifyModel
|
||||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||||
import com.afollestad.nocknock.utilities.providers.NotificationChannelProvider
|
import com.afollestad.nocknock.utilities.providers.NotificationChannelProvider
|
||||||
|
@ -42,13 +41,12 @@ import org.junit.Test
|
||||||
|
|
||||||
class NockNotificationManagerTest {
|
class NockNotificationManagerTest {
|
||||||
|
|
||||||
|
private val appIconRes = 1024
|
||||||
private val somethingWentWrong = "something went wrong"
|
private val somethingWentWrong = "something went wrong"
|
||||||
private val yay = "yay!"
|
|
||||||
|
|
||||||
private val stockManager = mock<NotificationManager>()
|
private val stockManager = mock<NotificationManager>()
|
||||||
private val stringProvider = mock<StringProvider> {
|
private val stringProvider = mock<StringProvider> {
|
||||||
on { get(R.string.something_wrong) } doReturn somethingWentWrong
|
on { get(R.string.something_wrong) } doReturn somethingWentWrong
|
||||||
on { get(R.string.validation_passed) } doReturn yay
|
|
||||||
}
|
}
|
||||||
private val intentProvider = mock<IntentProvider>()
|
private val intentProvider = mock<IntentProvider>()
|
||||||
private val channelProvider = mock<NotificationChannelProvider>()
|
private val channelProvider = mock<NotificationChannelProvider>()
|
||||||
|
@ -79,42 +77,30 @@ class NockNotificationManagerTest {
|
||||||
|
|
||||||
@Test fun createChannels() {
|
@Test fun createChannels() {
|
||||||
whenever(stringProvider.get(any())).doReturn("")
|
whenever(stringProvider.get(any())).doReturn("")
|
||||||
val errorChannel = mock<NotificationChannel> {
|
val createdChannel = mock<NotificationChannel> {
|
||||||
on { this.id } doReturn ValidationErrors.id
|
on { this.id } doReturn CheckFailures.id
|
||||||
}
|
|
||||||
val successChannel = mock<NotificationChannel> {
|
|
||||||
on { this.id } doReturn ValidationSuccess.id
|
|
||||||
}
|
|
||||||
|
|
||||||
whenever(channelProvider.create(any(), any(), any(), any())).doAnswer { inv ->
|
|
||||||
val channelId = inv.getArgument<String>(0)
|
|
||||||
when (channelId) {
|
|
||||||
ValidationErrors.id -> errorChannel
|
|
||||||
ValidationSuccess.id -> successChannel
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
whenever(channelProvider.create(any(), any(), any(), any()))
|
||||||
|
.doReturn(createdChannel)
|
||||||
manager.createChannels()
|
manager.createChannels()
|
||||||
|
|
||||||
val captor = argumentCaptor<NotificationChannel>()
|
val captor = argumentCaptor<NotificationChannel>()
|
||||||
verify(stockManager, times(2)).createNotificationChannel(captor.capture())
|
verify(stockManager, times(1)).createNotificationChannel(captor.capture())
|
||||||
|
|
||||||
val channels = captor.allValues
|
val channel = captor.allValues.single()
|
||||||
assertThat(channels.size).isEqualTo(2)
|
assertThat(channel.id).isEqualTo(CheckFailures.id)
|
||||||
assertThat(channels.first()).isEqualTo(successChannel)
|
|
||||||
assertThat(channels.last()).isEqualTo(errorChannel)
|
|
||||||
|
|
||||||
verifyNoMoreInteractions(stockManager)
|
verifyNoMoreInteractions(stockManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun postValidationSuccessNotification_appIsOpen() {
|
@Test fun postStatusNotification_appIsOpen() {
|
||||||
manager.setIsAppOpen(true)
|
manager.setIsAppOpen(true)
|
||||||
manager.postValidationSuccessNotification(fakeModel())
|
manager.postStatusNotification(fakeModel())
|
||||||
|
|
||||||
verifyNoMoreInteractions(stockManager)
|
verifyNoMoreInteractions(stockManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun postValidationSuccessNotification_appNotOpen() {
|
@Test fun postStatusNotification_appNotOpen() {
|
||||||
manager.setIsAppOpen(false)
|
manager.setIsAppOpen(false)
|
||||||
val model = fakeModel()
|
val model = fakeModel()
|
||||||
|
|
||||||
|
@ -125,15 +111,15 @@ class NockNotificationManagerTest {
|
||||||
val notification = mock<Notification>()
|
val notification = mock<Notification>()
|
||||||
whenever(
|
whenever(
|
||||||
notificationProvider.create(
|
notificationProvider.create(
|
||||||
ValidationErrors.id,
|
CheckFailures.id,
|
||||||
"Testing",
|
"Testing",
|
||||||
yay,
|
somethingWentWrong,
|
||||||
pendingIntent,
|
pendingIntent,
|
||||||
R.drawable.ic_notification_success
|
R.drawable.ic_notification
|
||||||
)
|
)
|
||||||
).doReturn(notification)
|
).doReturn(notification)
|
||||||
|
|
||||||
manager.postValidationSuccessNotification(model)
|
manager.postStatusNotification(model)
|
||||||
|
|
||||||
verify(stockManager).notify(
|
verify(stockManager).notify(
|
||||||
"https://hello.com",
|
"https://hello.com",
|
||||||
|
@ -143,13 +129,6 @@ class NockNotificationManagerTest {
|
||||||
verifyNoMoreInteractions(stockManager)
|
verifyNoMoreInteractions(stockManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun postValidationErrorNotification_appIsOpen() {
|
|
||||||
manager.setIsAppOpen(true)
|
|
||||||
manager.postValidationErrorNotification(fakeModel())
|
|
||||||
|
|
||||||
verifyNoMoreInteractions(stockManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun cancelStatusNotification() {
|
@Test fun cancelStatusNotification() {
|
||||||
val model = fakeModel()
|
val model = fakeModel()
|
||||||
manager.cancelStatusNotification(model)
|
manager.cancelStatusNotification(model)
|
||||||
|
@ -169,7 +148,5 @@ class NockNotificationManagerTest {
|
||||||
override fun notifyName() = "Testing"
|
override fun notifyName() = "Testing"
|
||||||
|
|
||||||
override fun notifyTag() = "https://hello.com"
|
override fun notifyTag() = "https://hello.com"
|
||||||
|
|
||||||
override fun notifyDescription() = "Hello, World!"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -18,8 +18,6 @@ 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
|
||||||
|
|
|
@ -17,24 +17,37 @@ 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
|
|
||||||
}
|
|
||||||
|
|
|
@ -56,23 +56,10 @@ class HeaderStackLayout(
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
override fun onClick(v: View) {
|
||||||
val index = v.tag as Int
|
val index = v.tag as Int
|
||||||
check(index >= 0 || index < list.childCount) {
|
|
||||||
"Index $index is out of bounds in the header stack (size ${list.childCount})."
|
|
||||||
}
|
|
||||||
list.post {
|
|
||||||
list.removeViewAt(index)
|
list.removeViewAt(index)
|
||||||
headers.removeAt(index)
|
headers.removeAt(index)
|
||||||
invalidateTags()
|
|
||||||
postLiveData()
|
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) {
|
||||||
// Keep track of reference for posting future changes.
|
// Keep track of reference for posting future changes.
|
||||||
|
@ -80,7 +67,9 @@ 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.apply {
|
list.addView(entry)
|
||||||
|
|
||||||
|
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)
|
||||||
|
@ -88,6 +77,6 @@ class HeaderStackLayout(
|
||||||
|
|
||||||
btnRemove.tag = headers.size - 1
|
btnRemove.tag = headers.size - 1
|
||||||
btnRemove.setOnClickListener(this@HeaderStackLayout)
|
btnRemove.setOnClickListener(this@HeaderStackLayout)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ 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
|
||||||
|
@ -27,7 +28,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.vvalidator.form.Form
|
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
|
||||||
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
|
||||||
|
|
||||||
|
@ -65,14 +66,8 @@ class ValidationIntervalLayout(
|
||||||
fun attach(
|
fun attach(
|
||||||
valueData: MutableLiveData<Int>,
|
valueData: MutableLiveData<Int>,
|
||||||
multiplierData: MutableLiveData<Long>,
|
multiplierData: MutableLiveData<Long>,
|
||||||
form: Form
|
errorData: LiveData<Int?>
|
||||||
) {
|
) {
|
||||||
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(),
|
||||||
|
@ -96,5 +91,10 @@ class ValidationIntervalLayout(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
errorData.toViewError(lifecycleOwner(), this, ::setError)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setError(error: String?) {
|
||||||
|
input.error = error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,17 +20,15 @@ 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
|
||||||
|
|
||||||
|
@ -52,25 +50,16 @@ class JavaScriptInputLayout(
|
||||||
contentInset // bottom
|
contentInset // bottom
|
||||||
)
|
)
|
||||||
elevation = dimenFloat(dimen.default_elevation)
|
elevation = dimenFloat(dimen.default_elevation)
|
||||||
inflate(context, R.layout.javascript_input_layout, this)
|
inflate(context, layout.javascript_input_layout, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun attach(
|
fun attach(
|
||||||
codeData: MutableLiveData<String>,
|
codeData: MutableLiveData<String>,
|
||||||
visibility: LiveData<Boolean>,
|
errorData: LiveData<Int?>,
|
||||||
form: Form
|
visibility: LiveData<Boolean>
|
||||||
) {
|
) {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ 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
|
||||||
|
@ -42,8 +41,7 @@ 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)
|
||||||
|
@ -52,13 +50,6 @@ 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() {
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
android:id="@+id/label"
|
android:id="@+id/label"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/validation_interval"
|
android:text="@string/check_interval"
|
||||||
style="@style/NockText.SectionHeader"
|
style="@style/NockText.SectionHeader"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/validation_interval_every"
|
android:text="@string/check_interval_every"
|
||||||
style="@style/NockText.Body"
|
style="@style/NockText.Body"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
<string name="function_declaration">function validate(response) {</string>
|
<string name="function_declaration">function validate(response) {</string>
|
||||||
<string name="function_end">}</string>
|
<string name="function_end">}</string>
|
||||||
|
|
||||||
<string name="validation_interval">Validation Interval</string>
|
<string name="check_interval">Check Interval</string>
|
||||||
<string name="validation_interval_every">Every</string>
|
<string name="check_interval_every">Every</string>
|
||||||
|
|
||||||
<string name="retry_policy">Retry Policy</string>
|
<string name="retry_policy">Retry Policy</string>
|
||||||
<string name="retry_policy_retry">Retry</string>
|
<string name="retry_policy_retry">Retry</string>
|
||||||
|
@ -23,8 +23,4 @@
|
||||||
<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>
|
||||||
|
|
|
@ -52,14 +52,4 @@
|
||||||
<item name="android:textColor">@color/md_red</item>
|
<item name="android:textColor">@color/md_red</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AccentButton" parent="Widget.MaterialComponents.Button">
|
|
||||||
<item name="android:textColor">#fff</item>
|
|
||||||
<item name="backgroundTint">?colorAccent</item>
|
|
||||||
<item name="android:fontFamily">@font/lato</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="AccentTextButton" parent="Widget.MaterialComponents.Button.TextButton">
|
|
||||||
<item name="android:fontFamily">@font/lato</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|