Compare commits

..

No commits in common. "master" and "0.8.2b" have entirely different histories.

129 changed files with 1377 additions and 2778 deletions

3
.editorconfig Normal file
View file

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

28
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,28 @@
(`[x]` becomes a filled in checkbox, `[ ]` is an empty one)
- [ ] I have verified there are [no duplicate active or recent bugs, questions, or requests](https://github.com/afollestad/nock-nock/issues?q=is%3Aissue+is%3Aclosed)
- [ ] I have given my issue a non-generic title.
---
If this is a improvement or feature request, you can remove everything below.
Also, please consider making a pull request if you are capable of contributing.
###### Include the following:
- Nock Nock version: `0.x.x`
- Affected device: Google Pixel 3 XL with Android 9.0
---
###### Reproduction Steps
1.
---
###### Expected Result
---
###### Actual Result

View file

@ -1,28 +0,0 @@
---
name: Bug report
about: Something is crashing or not working as intended
---
*Please consider making a Pull Request if you are capable of doing so.*
**App Version:**
x.x.x
**Affected Device(s):**
Google Pixel 3 XL with Android 9.0
**Describe the Bug:**
A clear description of what is the bug is.
**To Reproduce:**
1.
2.
3.
**Expected Behavior:**
A clear description of what you expected to happen.

View file

@ -1,15 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
---
*Please consider making a Pull Request if you are capable of doing so.*
**Description what you'd like to happen:**
A clear description if the feature or behavior you'd like implemented.
**Describe alternatives you've considered:**
A clear description of any alternative solutions you've considered.

View file

@ -1,8 +1,9 @@
### Guidelines
1. You must run the `spotlessApply` task before committing, either through Android Studio or with `./gradlew spotlessApply`.
1. You must run the `spotlessApply` task before commiting, either through Android Studio or with `./gradlew spotlessApply`.
2. A PR should be focused and contained. If you are changing multiple unrelated things, they should be in separate PRs.
3. A PR should fix a bug or solve a problem - something that only you would use is not necessarily something that should be published.
4. Give your PR a detailed title and description - look over your code one last time before actually creating the PR. Give it a self-review.
**If you do not follow the guidelines, your PR will be rejected.**
**If you do not follow the guidelines, your PR will be rejected.**

4
.gitignore vendored
View file

@ -180,6 +180,4 @@ gradle-app.setting
.gradletasknamecache
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties
app/google-services.json
# gradle/wrapper/gradle-wrapper.properties

37
.idea/misc.xml generated
View file

@ -5,42 +5,7 @@
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
</configurations>
</component>
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="10">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="9">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
</list>
</value>
</option>
</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" />
</component>
<component name="ProjectType">

19
.travis.yml Normal file
View 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:
- '.+'

View file

@ -1,8 +1,9 @@
## Nock Nock
[![Build Status](https://travis-ci.org/afollestad/nock-nock.svg)](https://travis-ci.org/afollestad/nock-nock)
[![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg?style=flat-square)](https://www.apache.org/licenses/LICENSE-2.0.html)
![Showcase](https://raw.githubusercontent.com/afollestad/nock-nock/master/art/showcase5.png)
![Showcase](https://raw.githubusercontent.com/afollestad/nock-nock/master/art/showcasemain3.png)
Nock Nock is a simple app which allows you to monitor your websites for maximum uptime.

View file

@ -4,6 +4,18 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'io.fabric'
def getFabricApiKey() {
def propsFile = project.rootProject.file('local.properties')
if (!propsFile.exists()) {
return ""
}
Properties properties = new Properties()
properties.load(propsFile.newDataInputStream())
return properties.getProperty("fabric.apikey") ?: ""
}
android {
compileSdkVersion versions.compileSdk
buildToolsVersion versions.buildTools
@ -14,15 +26,16 @@ android {
targetSdkVersion versions.compileSdk
versionCode versions.publishVersionCode
versionName versions.publishVersion
manifestPlaceholders = [fabricKey:getFabricApiKey()]
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
buildTypes {
debug {
buildConfigField "String", "FABRIC_API_KEY", "\"\""
}
release {
buildConfigField "String", "FABRIC_API_KEY", "\"${getFabricApiKey()}\""
}
}
}
@ -38,7 +51,6 @@ dependencies {
implementation 'androidx.recyclerview:recyclerview:' + versions.androidxRecyclerView
implementation 'com.google.android.material:material:' + versions.googleMaterial
implementation 'androidx.browser:browser:' + versions.androidxBrowser
implementation 'com.google.firebase:firebase-core:' + versions.firebaseCore
// Lifecycle
kapt 'androidx.lifecycle:lifecycle-compiler:' + versions.lifecycle
@ -72,8 +84,4 @@ dependencies {
androidTestImplementation 'androidx.test:rules:' + versions.androidxTestRunner
}
apply from: '../spotless.gradle'
apply from: '../mock/mock.gradle'
apply plugin: "io.fabric"
apply plugin: 'com.google.gms.google-services'
apply from: '../spotless.gradle'

View file

@ -50,6 +50,9 @@
</intent-filter>
</receiver>
<meta-data
android:name="io.fabric.ApiKey"
android:value="${fabricKey}"/>
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts"/>

View file

@ -20,12 +20,12 @@ import android.app.Application
import android.app.Application.ActivityLifecycleCallbacks
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
import androidx.core.text.HtmlCompat.fromHtml
import com.afollestad.materialdialogs.utils.MDUtil.resolveColor
import com.afollestad.nocknock.utilities.ext.toUri
import com.afollestad.nocknock.utilities.ui.toast
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.toUri() = Uri.parse(this)!!
fun Activity.viewUrl(url: String) {
val customTabsIntent = CustomTabsIntent.Builder()
.apply {

View file

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

View file

@ -1,115 +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.adapter
import android.graphics.Color.WHITE
import android.view.LayoutInflater
import android.view.View
import android.view.View.OnClickListener
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.afollestad.nocknock.R
import com.afollestad.nocknock.adapter.TagAdapter.TagViewHolder
import kotlinx.android.synthetic.main.list_item_tag.view.chip
typealias TagsListener = (tags: List<String>) -> Unit
/** @author Aidan Follestad (@afollestad) */
class TagAdapter(
private val listener: TagsListener
) : RecyclerView.Adapter<TagViewHolder>() {
private val tags = mutableListOf<String>()
private val checked = mutableListOf<Int>()
fun set(tags: List<String>) {
this.tags.run {
clear()
addAll(tags)
}
notifyDataSetChanged()
}
fun toggleChecked(index: Int) {
if (checked.contains(index)) {
checked.remove(index)
} else {
checked.add(index)
}
notifyItemChanged(index)
listener.invoke(getCheckedTags())
}
private fun getCheckedTags(): List<String> {
return mutableListOf<String>().apply {
checked.forEach { index -> add(tags[index]) }
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): TagViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item_tag, parent, false)
return TagViewHolder(view, this)
}
override fun getItemCount() = tags.size
override fun onBindViewHolder(
holder: TagViewHolder,
position: Int
) {
holder.bind(tags[position], checked.contains(position))
}
/** @author Aidan Follestad (@afollestad) */
class TagViewHolder(
itemView: View,
private val adapter: TagAdapter
) : ViewHolder(itemView), OnClickListener {
override fun onClick(v: View) = adapter.toggleChecked(adapterPosition)
init {
itemView.setOnClickListener(this)
}
fun bind(
name: String,
checked: Boolean
) = itemView.chip.run {
text = name
setTextColor(
if (checked) {
WHITE
} else {
ContextCompat.getColor(itemView.context, R.color.unchecked_chip_text)
}
)
setBackgroundResource(
if (checked) {
R.drawable.checked_chip_selector
} else {
R.drawable.unchecked_chip_selector
}
)
}
}
}

View file

@ -20,7 +20,6 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.DialogFragment
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.nocknock.BuildConfig
import com.afollestad.nocknock.R
/** @author Aidan Follestad (@afollestad) */
@ -35,9 +34,8 @@ class AboutDialog : DialogFragment() {
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val context = activity ?: throw IllegalStateException("Oh no!")
return MaterialDialog(context)
.title(text = getString(R.string.app_name_x, BuildConfig.VERSION_NAME))
return MaterialDialog(activity!!)
.title(R.string.about)
.positiveButton(R.string.dismiss)
.message(R.string.about_body, html = true, lineHeightMultiplier = 1.4f)
}

View file

@ -23,9 +23,6 @@ import android.content.Context.NOTIFICATION_SERVICE
import androidx.room.Room.databaseBuilder
import com.afollestad.nocknock.data.AppDatabase
import com.afollestad.nocknock.data.Database1to2Migration
import com.afollestad.nocknock.data.Database2to3Migration
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.ui.main.MainActivity
import com.afollestad.nocknock.utilities.ext.systemService
@ -41,12 +38,7 @@ val mainModule = module {
single {
databaseBuilder(get(), AppDatabase::class.java, "NockNock.db")
.addMigrations(
Database1to2Migration(),
Database2to3Migration(),
Database3to4Migration(),
Database4to5Migration()
)
.addMigrations(Database1to2Migration())
.build()
}

View file

@ -15,15 +15,10 @@
*/
package com.afollestad.nocknock.ui
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.afollestad.nocknock.R
import com.afollestad.nocknock.koin.PREF_DARK_MODE
import com.afollestad.nocknock.ui.NightMode.DISABLED
import com.afollestad.nocknock.ui.NightMode.ENABLED
import com.afollestad.nocknock.ui.NightMode.UNKNOWN
import com.afollestad.nocknock.utilities.rx.attachLifecycle
import com.afollestad.rxkprefs.Pref
import org.koin.android.ext.android.inject
@ -40,35 +35,16 @@ abstract class DarkModeSwitchActivity : AppCompatActivity() {
setTheme(themeRes())
super.onCreate(savedInstanceState)
if (getCurrentNightMode() == UNKNOWN) {
darkModePref.observe()
.filter { it != isDarkModeEnabled }
.subscribe {
log("Theme changed, recreating Activity.")
recreate()
}
.attachLifecycle(this)
}
darkModePref.observe()
.filter { it != isDarkModeEnabled }
.subscribe {
log("Theme changed, recreating Activity.")
recreate()
}
.attachLifecycle(this)
}
protected fun getCurrentNightMode(): NightMode {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return UNKNOWN
}
return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES -> return ENABLED
Configuration.UI_MODE_NIGHT_NO -> return DISABLED
else -> UNKNOWN
}
}
protected fun isDarkMode(): Boolean {
return when (getCurrentNightMode()) {
ENABLED -> true
DISABLED -> false
else -> darkModePref.get()
}
}
protected fun isDarkMode() = darkModePref.get()
protected fun toggleDarkMode() = setDarkMode(!isDarkMode())

View file

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

View file

@ -16,32 +16,18 @@
package com.afollestad.nocknock.ui.addsite
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.widget.ArrayAdapter
import androidx.lifecycle.Observer
import com.afollestad.nocknock.R
import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.data.model.ValidationMode
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
import com.afollestad.nocknock.ui.viewsite.KEY_SITE
import com.afollestad.nocknock.utilities.ext.onTextChanged
import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
import com.afollestad.nocknock.utilities.livedata.distinct
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
import com.afollestad.nocknock.viewcomponents.ext.isVisibleCondition
import com.afollestad.nocknock.viewcomponents.ext.onScroll
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
import com.afollestad.vvalidator.form
import com.afollestad.vvalidator.form.Form
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout
import kotlinx.android.synthetic.main.activity_addsite.headersLayout
import kotlinx.android.synthetic.main.activity_addsite.doneBtn
import kotlinx.android.synthetic.main.activity_addsite.inputName
import kotlinx.android.synthetic.main.activity_addsite.inputTags
import kotlinx.android.synthetic.main.activity_addsite.inputUrl
import kotlinx.android.synthetic.main.activity_addsite.loadingProgress
import kotlinx.android.synthetic.main.activity_addsite.responseTimeoutInput
@ -49,54 +35,44 @@ import kotlinx.android.synthetic.main.activity_addsite.responseValidationMode
import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchTerm
import kotlinx.android.synthetic.main.activity_addsite.retryPolicyLayout
import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout
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.validationModeDescription
import kotlinx.android.synthetic.main.include_app_bar.toolbar
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
/** @author Aidan Follestad (@afollestad) */
class AddSiteActivity : DarkModeSwitchActivity() {
companion object {
private const val SELECT_CERT_FILE_RQ = 23
}
private val viewModel by viewModel<AddSiteViewModel>()
private lateinit var validationForm: Form
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_addsite)
setupUi()
setupValidation()
lifecycle.addObserver(viewModel)
// Populate view model with initial data
val model = intent.getSerializableExtra(KEY_SITE) as? Site
model?.let { viewModel.prePopulateFromModel(model) }
// Loading
loadingProgress.observe(this, viewModel.onIsLoading())
// Name
inputName.attachLiveData(this, viewModel.name)
// Tags
inputTags.attachLiveData(this, viewModel.tags)
viewModel.onNameError()
.toViewError(this, inputName)
// Url
inputUrl.attachLiveData(this, viewModel.url)
viewModel.onUrlError()
.toViewError(this, inputUrl)
viewModel.onUrlWarningVisibility()
.toViewVisibility(this, textUrlWarning)
// Timeout
responseTimeoutInput.attachLiveData(this, viewModel.timeout)
viewModel.onTimeoutError()
.toViewError(this, responseTimeoutInput)
// Validation mode
responseValidationMode.attachLiveData(
@ -105,6 +81,8 @@ class AddSiteActivity : DarkModeSwitchActivity() {
outTransformer = { ValidationMode.fromIndex(it) },
inTransformer = { it.toIndex() }
)
viewModel.onValidationSearchTermError()
.toViewError(this, responseValidationSearchTerm)
viewModel.onValidationModeDescription()
.toViewText(this, validationModeDescription)
@ -117,19 +95,30 @@ class AddSiteActivity : DarkModeSwitchActivity() {
viewModel.onValidationSearchTermVisibility()
.toViewVisibility(this, responseValidationSearchTerm)
// SSL certificate
sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it }
viewModel.certificateUri.distinct()
.observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) })
// Validation script
scriptInputLayout.attach(
codeData = viewModel.validationScript,
errorData = viewModel.onValidationScriptError(),
visibility = viewModel.onValidationScriptVisibility()
)
// Headers
headersLayout.attach(viewModel.headers)
// Check interval
checkIntervalLayout.attach(
valueData = viewModel.checkIntervalValue,
multiplierData = viewModel.checkIntervalUnit,
errorData = viewModel.onCheckIntervalError()
)
// Retry Policy
retryPolicyLayout.attach(
timesData = viewModel.retryPolicyTimes,
minutesData = viewModel.retryPolicyMinutes
)
}
private fun setupUi() {
toolbarTitle.setText(R.string.add_site)
toolbar.run {
inflateMenu(R.menu.menu_addsite)
setNavigationIcon(R.drawable.ic_action_close)
setNavigationOnClickListener { finish() }
}
@ -142,94 +131,12 @@ class AddSiteActivity : DarkModeSwitchActivity() {
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
responseValidationMode.adapter = validationOptionsAdapter
scrollView.onScroll {
appToolbar.elevation = if (it > appToolbar.measuredHeight / 2) {
appToolbar.dimenFloat(R.dimen.default_elevation)
} else {
0f
// Done button
doneBtn.setOnClickListener {
viewModel.commit {
setResult(RESULT_OK)
finish()
}
}
// 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() ?: "")
}
}
}

View file

@ -25,8 +25,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.OnLifecycleEvent
import com.afollestad.nocknock.R
import com.afollestad.nocknock.data.AppDatabase
import com.afollestad.nocknock.data.model.Header
import com.afollestad.nocknock.data.model.RetryPolicy
import com.afollestad.nocknock.data.RetryPolicy
import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.data.model.SiteSettings
import com.afollestad.nocknock.data.model.Status.WAITING
@ -36,10 +35,11 @@ import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
import com.afollestad.nocknock.data.model.ValidationResult
import com.afollestad.nocknock.data.putSite
import com.afollestad.nocknock.engine.validation.ValidationExecutor
import com.afollestad.nocknock.engine.validation.ValidationManager
import com.afollestad.nocknock.ui.ScopedViewModel
import com.afollestad.nocknock.utilities.ext.MINUTE
import com.afollestad.nocknock.utilities.livedata.map
import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -49,14 +49,13 @@ import java.lang.System.currentTimeMillis
/** @author Aidan Follestad (@afollestad) */
class AddSiteViewModel(
private val database: AppDatabase,
private val validationManager: ValidationExecutor,
private val validationManager: ValidationManager,
mainDispatcher: CoroutineDispatcher,
private val ioDispatcher: CoroutineDispatcher
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
// Public properties
val name = MutableLiveData<String>()
val tags = MutableLiveData<String>()
val url = MutableLiveData<String>()
val timeout = MutableLiveData<Int>()
val validationMode = MutableLiveData<ValidationMode>()
@ -66,8 +65,6 @@ class AddSiteViewModel(
val checkIntervalUnit = MutableLiveData<Long>()
val retryPolicyTimes = MutableLiveData<Int>()
val retryPolicyMinutes = MutableLiveData<Int>()
val headers = MutableLiveData<List<Header>>()
val certificateUri = MutableLiveData<String>()
@OnLifecycleEvent(ON_START)
fun setDefaults() {
@ -77,14 +74,24 @@ class AddSiteViewModel(
checkIntervalUnit.value = MINUTE
retryPolicyMinutes.value = 0
retryPolicyMinutes.value = 0
tags.value = ""
headers.value = emptyList()
}
// Private properties
private val isLoading = MutableLiveData<Boolean>()
private val nameError = MutableLiveData<Int?>()
private val urlError = MutableLiveData<Int?>()
private val timeoutError = MutableLiveData<Int?>()
private val validationSearchTermError = MutableLiveData<Int?>()
private val validationScriptError = MutableLiveData<Int?>()
private val checkIntervalValueError = MutableLiveData<Int?>()
// Expose private properties or calculated properties
@CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading
@CheckResult fun onNameError(): LiveData<Int?> = nameError
@CheckResult fun onUrlError(): LiveData<Int?> = urlError
@CheckResult fun onUrlWarningVisibility(): LiveData<Boolean> {
return url.map {
val parsed = HttpUrl.parse(it)
@ -92,6 +99,8 @@ class AddSiteViewModel(
}
}
@CheckResult fun onTimeoutError(): LiveData<Int?> = timeoutError
@CheckResult fun onValidationModeDescription(): LiveData<Int> {
return validationMode.map {
when (it!!) {
@ -102,9 +111,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
fun commit(done: () -> Unit) {
@ -115,7 +132,7 @@ class AddSiteViewModel(
val storedModel = withContext(ioDispatcher) {
database.putSite(newModel)
}
validationManager.scheduleValidation(
validationManager.scheduleCheck(
site = storedModel,
rightNow = true,
cancelPrevious = true
@ -144,16 +161,75 @@ class AddSiteViewModel(
}
private fun generateDbModel(): Site? {
val timeout = timeout.value ?: 10_000
val cleanedTags = tags.value?.split(',')?.joinToString { it.trim() } ?: ""
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
if (timeout.value.isNullOrLessThan(1)) {
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 newSettings = SiteSettings(
validationIntervalMs = getCheckIntervalMs(),
validationMode = validationMode.value!!,
validationArgs = getValidationArgs(),
networkTimeout = timeout,
disabled = false,
certificate = certificateUri.value?.toString()
networkTimeout = timeout.value!!,
disabled = false
)
val newLastResult = ValidationResult(
@ -165,10 +241,7 @@ class AddSiteViewModel(
val retryPolicyTimes = retryPolicyTimes.value ?: 0
val retryPolicyMinutes = retryPolicyMinutes.value ?: 0
val newRetryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) {
RetryPolicy(
count = retryPolicyTimes,
minutes = retryPolicyMinutes
)
RetryPolicy(count = retryPolicyTimes, minutes = retryPolicyMinutes)
} else {
null
}
@ -177,11 +250,9 @@ class AddSiteViewModel(
id = 0,
name = name.value!!.trim(),
url = url.value!!.trim(),
tags = cleanedTags,
settings = newSettings,
lastResult = newLastResult,
retryPolicy = newRetryPolicy,
headers = headers.value ?: emptyList()
retryPolicy = newRetryPolicy
)
}
}

View file

@ -1,99 +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.addsite
import com.afollestad.nocknock.data.model.RetryPolicy
import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
import com.afollestad.nocknock.utilities.ext.DAY
import com.afollestad.nocknock.utilities.ext.HOUR
import com.afollestad.nocknock.utilities.ext.MINUTE
import com.afollestad.nocknock.utilities.ext.WEEK
import kotlin.math.ceil
fun AddSiteViewModel.prePopulateFromModel(site: Site) {
val settings = site.settings ?: throw IllegalArgumentException("Settings must be populated!")
name.value = site.name
tags.value = site.tags
url.value = site.url
timeout.value = settings.networkTimeout
validationMode.value = settings.validationMode
when (settings.validationMode) {
TERM_SEARCH -> {
validationSearchTerm.value = settings.validationArgs
validationScript.value = null
}
JAVASCRIPT -> {
validationSearchTerm.value = null
validationScript.value = settings.validationArgs
}
else -> {
validationSearchTerm.value = null
validationScript.value = null
}
}
setCheckInterval(settings.validationIntervalMs)
setRetryPolicy(site.retryPolicy)
headers.value = site.headers
}
private fun AddSiteViewModel.setCheckInterval(interval: Long) {
when {
interval >= WEEK -> {
checkIntervalValue.value =
getIntervalFromUnit(interval, WEEK)
checkIntervalUnit.value = WEEK
}
interval >= DAY -> {
checkIntervalValue.value =
getIntervalFromUnit(interval, DAY)
checkIntervalUnit.value = DAY
}
interval >= HOUR -> {
checkIntervalValue.value =
getIntervalFromUnit(interval, HOUR)
checkIntervalUnit.value = HOUR
}
interval >= MINUTE -> {
checkIntervalValue.value =
getIntervalFromUnit(interval, MINUTE)
checkIntervalUnit.value = MINUTE
}
else -> {
checkIntervalValue.value = 0
checkIntervalUnit.value = MINUTE
}
}
}
private fun AddSiteViewModel.setRetryPolicy(policy: RetryPolicy?) {
if (policy == null) return
retryPolicyTimes.value = policy.count
retryPolicyMinutes.value = policy.minutes
}
private fun getIntervalFromUnit(
millis: Long,
unit: Long
): Int {
val intervalFloat = millis.toFloat()
val byFloat = unit.toFloat()
return ceil(intervalFloat / byFloat).toInt()
}

View file

@ -21,19 +21,20 @@ import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems
import com.afollestad.materialdialogs.list.listItemsSingleChoice
import com.afollestad.nocknock.R
import com.afollestad.nocknock.adapter.SiteAdapter
import com.afollestad.nocknock.adapter.TagAdapter
import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver
import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.dialogs.AboutDialog
import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
import com.afollestad.nocknock.ui.NightMode.UNKNOWN
import com.afollestad.nocknock.utilities.providers.IntentProvider
import com.afollestad.nocknock.utilities.ui.toast
import com.afollestad.nocknock.viewUrl
import com.afollestad.nocknock.viewUrlWithApp
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
import kotlinx.android.synthetic.main.activity_main.fab
import kotlinx.android.synthetic.main.activity_main.list
@ -42,7 +43,6 @@ import kotlinx.android.synthetic.main.include_app_bar.toolbar
import kotlinx.android.synthetic.main.include_empty_view.emptyText
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlinx.android.synthetic.main.activity_main.tags_list as tagsList
/** @author Aidan Follestad (@afollestad) */
class MainActivity : DarkModeSwitchActivity() {
@ -53,7 +53,6 @@ class MainActivity : DarkModeSwitchActivity() {
internal val viewModel by viewModel<MainViewModel>()
private lateinit var siteAdapter: SiteAdapter
private lateinit var tagAdapter: TagAdapter
private val statusUpdateReceiver by lazy {
StatusUpdateIntentReceiver(application, intentProvider) {
@ -77,10 +76,6 @@ class MainActivity : DarkModeSwitchActivity() {
.observe(this, Observer { siteAdapter.set(it) })
viewModel.onEmptyTextVisibility()
.toViewVisibility(this, emptyText)
viewModel.onTags()
.observe(this, Observer { tagAdapter.set(it) })
viewModel.onTagsListVisibility()
.toViewVisibility(this, tagsList)
loadingProgress.observe(this, viewModel.onIsLoading())
processIntent(intent)
@ -90,35 +85,24 @@ class MainActivity : DarkModeSwitchActivity() {
toolbar.run {
inflateMenu(R.menu.menu_main)
menu.findItem(R.id.dark_mode)
.apply {
if (getCurrentNightMode() == UNKNOWN) {
isChecked = isDarkMode()
} else {
isVisible = false
}
}
.isChecked = isDarkMode()
setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.about -> AboutDialog.show(this@MainActivity)
R.id.dark_mode -> toggleDarkMode()
R.id.support_me -> supportMe()
}
return@setOnMenuItemClickListener true
}
}
siteAdapter = SiteAdapter(this::onSiteSelected)
list.run {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = siteAdapter
addItemDecoration(DividerItemDecoration(this@MainActivity, VERTICAL))
}
tagAdapter = TagAdapter(viewModel::onTagSelection)
tagsList.run {
layoutManager = LinearLayoutManager(this@MainActivity, HORIZONTAL, false)
adapter = tagAdapter
}
fab.setOnClickListener { addSite() }
}
@ -137,8 +121,7 @@ class MainActivity : DarkModeSwitchActivity() {
listItems(R.array.site_long_options) { _, i, _ ->
when (i) {
0 -> viewModel.refreshSite(model)
1 -> addSiteForDuplication(model)
2 -> maybeRemoveSite(model)
1 -> maybeRemoveSite(model)
}
}
}
@ -146,4 +129,20 @@ class MainActivity : DarkModeSwitchActivity() {
viewSite(model)
}
}
private fun supportMe() {
MaterialDialog(this).show {
title(R.string.support_me)
message(R.string.support_me_message, html = true, lineHeightMultiplier = 1.4f)
listItemsSingleChoice(R.array.donation_options) { _, index, _ ->
when (index) {
0 -> viewUrl("https://paypal.me/AidanFollestad")
1 -> viewUrlWithApp("https://cash.me/\$afollestad", pkg = "com.squareup.cash")
2 -> viewUrlWithApp("https://venmo.com/afollestad", pkg = "com.venmo")
}
toast(R.string.thank_you)
}
positiveButton(R.string.next)
}
}
}

View file

@ -28,23 +28,10 @@ import com.afollestad.nocknock.utilities.providers.RealIntentProvider.Companion.
internal const val VIEW_SITE_RQ = 6923
internal const val ADD_SITE_RQ = 6969
// ADD
internal fun MainActivity.addSite() {
startActivityForResult(intentToAdd(), ADD_SITE_RQ)
startActivityForResult(Intent(this, AddSiteActivity::class.java), ADD_SITE_RQ)
}
internal fun MainActivity.addSiteForDuplication(site: Site) {
startActivityForResult(intentToAdd(site), ADD_SITE_RQ)
}
private fun MainActivity.intentToAdd(model: Site? = null) =
Intent(this, AddSiteActivity::class.java).apply {
model?.let { putExtra(KEY_SITE, it) }
}
// VIEW
internal fun MainActivity.viewSite(model: Site) {
startActivityForResult(intentToView(model), VIEW_SITE_RQ)
}
@ -54,8 +41,6 @@ private fun MainActivity.intentToView(model: Site) =
putExtra(KEY_SITE, model)
}
// MISC
internal fun MainActivity.maybeRemoveSite(model: Site) {
MaterialDialog(this).show {
title(R.string.remove_site)

View file

@ -25,7 +25,7 @@ import com.afollestad.nocknock.data.AppDatabase
import com.afollestad.nocknock.data.allSites
import com.afollestad.nocknock.data.deleteSite
import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.engine.validation.ValidationExecutor
import com.afollestad.nocknock.engine.validation.ValidationManager
import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.ui.ScopedViewModel
import kotlinx.coroutines.CoroutineDispatcher
@ -36,7 +36,7 @@ import kotlinx.coroutines.withContext
class MainViewModel(
private val database: AppDatabase,
private val notificationManager: NockNotificationManager,
private val validationManager: ValidationExecutor,
private val validationManager: ValidationManager,
mainDispatcher: CoroutineDispatcher,
private val ioDispatcher: CoroutineDispatcher
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
@ -44,8 +44,6 @@ class MainViewModel(
private val sites = MutableLiveData<List<Site>>()
private val isLoading = MutableLiveData<Boolean>()
private val emptyTextVisibility = MutableLiveData<Boolean>()
private val tags = MutableLiveData<List<String>>()
private val tagsListVisibility = MutableLiveData<Boolean>()
@CheckResult fun onSites(): LiveData<List<Site>> = sites
@ -53,14 +51,8 @@ class MainViewModel(
@CheckResult fun onEmptyTextVisibility(): LiveData<Boolean> = emptyTextVisibility
@CheckResult fun onTags(): LiveData<List<String>> = tags
@CheckResult fun onTagsListVisibility(): LiveData<Boolean> = tagsListVisibility
@OnLifecycleEvent(ON_RESUME)
fun onResume() = loadSites(emptyList())
fun onTagSelection(tags: List<String>) = loadSites(tags)
fun onResume() = loadSites()
fun postSiteUpdate(model: Site) {
val currentSites = sites.value ?: return
@ -73,7 +65,7 @@ class MainViewModel(
}
fun refreshSite(model: Site) {
validationManager.scheduleValidation(
validationManager.scheduleCheck(
site = model,
rightNow = true,
cancelPrevious = true
@ -81,7 +73,7 @@ class MainViewModel(
}
fun removeSite(model: Site) {
validationManager.cancelScheduledValidation(model)
validationManager.cancelCheck(model)
notificationManager.cancelStatusNotification(model)
scope.launch {
@ -102,56 +94,27 @@ class MainViewModel(
}
}
private fun loadSites(forTags: List<String>) {
private fun loadSites() {
scope.launch {
notificationManager.cancelStatusNotifications()
sites.value = listOf()
emptyTextVisibility.value = false
isLoading.value = true
val unfiltered = withContext(ioDispatcher) {
val result = withContext(ioDispatcher) {
database.allSites()
}
var result = unfiltered
if (forTags.isNotEmpty()) {
result = result.filter { site ->
val itemTags = site.tags.toLowerCase()
.split(",")
itemTags.any { tag -> forTags.contains(tag) }
}
}
sites.value = result
ensureCheckJobs()
isLoading.value = false
emptyTextVisibility.value = result.isEmpty()
val tagsValues = pullOutTags(unfiltered)
tags.value = tagsValues
tagsListVisibility.value = tagsValues.isNotEmpty()
}
}
private suspend fun ensureCheckJobs() {
withContext(ioDispatcher) {
validationManager.ensureScheduledValidations()
}
}
private fun pullOutTags(sites: List<Site>): List<String> {
return mutableListOf<String>().apply {
for (site in sites) {
val splitTags = site.tags.toLowerCase()
.split(',')
splitTags
.filter { it.isNotEmpty() }
.forEach { tag ->
if (!this.contains(tag)) {
this.add(tag)
}
}
}
sort()
validationManager.ensureScheduledChecks()
}
}
}

View file

@ -17,8 +17,6 @@ package com.afollestad.nocknock.ui.viewsite
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.widget.ArrayAdapter
import androidx.lifecycle.Observer
@ -27,23 +25,18 @@ import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver
import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.data.model.ValidationMode
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
import com.afollestad.nocknock.utilities.ext.onTextChanged
import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
import com.afollestad.nocknock.utilities.livedata.distinct
import com.afollestad.nocknock.utilities.providers.IntentProvider
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
import com.afollestad.nocknock.viewcomponents.ext.isVisibleCondition
import com.afollestad.nocknock.viewcomponents.ext.onScroll
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
import com.afollestad.vvalidator.form
import com.afollestad.vvalidator.form.Form
import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalLayout
import kotlinx.android.synthetic.main.activity_viewsite.headersLayout
import kotlinx.android.synthetic.main.activity_viewsite.disableChecksButton
import kotlinx.android.synthetic.main.activity_viewsite.doneBtn
import kotlinx.android.synthetic.main.activity_viewsite.iconStatus
import kotlinx.android.synthetic.main.activity_viewsite.inputName
import kotlinx.android.synthetic.main.activity_viewsite.inputTags
import kotlinx.android.synthetic.main.activity_viewsite.inputUrl
import kotlinx.android.synthetic.main.activity_viewsite.loadingProgress
import kotlinx.android.synthetic.main.activity_viewsite.responseTimeoutInput
@ -52,8 +45,6 @@ import kotlinx.android.synthetic.main.activity_viewsite.responseValidationSearch
import kotlinx.android.synthetic.main.activity_viewsite.retryPolicyLayout
import kotlinx.android.synthetic.main.activity_viewsite.scriptInputLayout
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.textNextCheck
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
@ -61,17 +52,12 @@ import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescriptio
import kotlinx.android.synthetic.main.include_app_bar.toolbar
import org.koin.android.ext.android.inject
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
/** @author Aidan Follestad (@afollestad) */
class ViewSiteActivity : DarkModeSwitchActivity() {
companion object {
private const val SELECT_CERT_FILE_RQ = 23
}
internal val viewModel by viewModel<ViewSiteViewModel>()
private lateinit var validationForm: Form
private val intentProvider by inject<IntentProvider>()
private val statusUpdateReceiver by lazy {
@ -84,18 +70,17 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_viewsite)
// Populate view model with initial data
val model = intent.getSerializableExtra(KEY_SITE) as Site
viewModel.setModel(model)
setupUi()
setupValidation()
lifecycle.run {
addObserver(viewModel)
addObserver(statusUpdateReceiver)
}
// Populate view model with initial data
val model = intent.getSerializableExtra(KEY_SITE) as Site
viewModel.setModel(model)
// Loading
loadingProgress.observe(this, viewModel.onIsLoading())
@ -107,17 +92,20 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
// Name
inputName.attachLiveData(this, viewModel.name)
// Tags
inputTags.attachLiveData(this, viewModel.tags)
viewModel.onNameError()
.toViewError(this, inputName)
// Url
inputUrl.attachLiveData(this, viewModel.url)
viewModel.onUrlError()
.toViewError(this, inputUrl)
viewModel.onUrlWarningVisibility()
.toViewVisibility(this, textUrlWarning)
// Timeout
responseTimeoutInput.attachLiveData(this, viewModel.timeout)
viewModel.onTimeoutError()
.toViewError(this, responseTimeoutInput)
// Validation mode
responseValidationMode.attachLiveData(
@ -126,6 +114,8 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
outTransformer = { ValidationMode.fromIndex(it) },
inTransformer = { it.toIndex() }
)
viewModel.onValidationSearchTermError()
.toViewError(this, responseValidationSearchTerm)
viewModel.onValidationModeDescription()
.toViewText(this, validationModeDescription)
@ -134,13 +124,25 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
viewModel.onValidationSearchTermVisibility()
.toViewVisibility(this, responseValidationSearchTerm)
// SSL certificate
sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it }
viewModel.certificateUri.distinct()
.observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) })
// Validation script
scriptInputLayout.attach(
codeData = viewModel.validationScript,
errorData = viewModel.onValidationScriptError(),
visibility = viewModel.onValidationScriptVisibility()
)
// Headers
headersLayout.attach(viewModel.headers)
// Check interval
checkIntervalLayout.attach(
valueData = viewModel.checkIntervalValue,
multiplierData = viewModel.checkIntervalUnit,
errorData = viewModel.onCheckIntervalError()
)
// Retry Policy
retryPolicyLayout.attach(
timesData = viewModel.retryPolicyTimes,
minutesData = viewModel.retryPolicyMinutes
)
// Last/next check
viewModel.onLastCheckResultText()
@ -150,30 +152,25 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
}
private fun setupUi() {
toolbarTitle.text = ""
toolbarTitle.setText(R.string.view_site)
toolbar.run {
setNavigationIcon(R.drawable.ic_action_close)
setNavigationOnClickListener { finish() }
inflateMenu(R.menu.menu_viewsite)
menu.findItem(R.id.refresh)
.setActionView(R.layout.menu_item_refresh_icon)
.apply {
actionView.setOnClickListener { viewModel.checkNow() }
}
setOnMenuItemClickListener {
when (it.itemId) {
R.id.remove -> maybeRemoveSite()
R.id.disableChecks -> maybeDisableChecks()
}
maybeRemoveSite()
true
}
}
scrollView.onScroll {
appToolbar.elevation = if (it > appToolbar.measuredHeight / 2) {
appToolbar.dimenFloat(R.dimen.default_elevation)
toolbar.elevation = if (it > toolbar.height / 4) {
toolbar.dimenFloat(R.dimen.default_elevation)
} else {
0f
}
@ -189,95 +186,14 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
// Disabled button
viewModel.onDisableChecksVisibility()
.observe(this, Observer {
toolbar.menu.findItem(R.id.disableChecks)
.isVisible = it
})
.toViewVisibility(this, disableChecksButton)
disableChecksButton.setOnClickListener { maybeDisableChecks() }
// Done item text
// Done button
viewModel.onDoneButtonText()
.observe(this, Observer {
toolbar.menu.findItem(R.id.commit)
.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() ?: "")
.toViewText(this, doneBtn)
doneBtn.setOnClickListener {
viewModel.commit { finish() }
}
}

View file

@ -23,9 +23,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.afollestad.nocknock.R
import com.afollestad.nocknock.data.AppDatabase
import com.afollestad.nocknock.data.RetryPolicy
import com.afollestad.nocknock.data.deleteSite
import com.afollestad.nocknock.data.model.Header
import com.afollestad.nocknock.data.model.RetryPolicy
import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.data.model.Status
import com.afollestad.nocknock.data.model.Status.WAITING
@ -36,13 +35,14 @@ import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
import com.afollestad.nocknock.data.model.ValidationResult
import com.afollestad.nocknock.data.model.textRes
import com.afollestad.nocknock.data.updateSite
import com.afollestad.nocknock.engine.validation.ValidationExecutor
import com.afollestad.nocknock.engine.validation.ValidationManager
import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.ui.ScopedViewModel
import com.afollestad.nocknock.utilities.ext.formatDate
import com.afollestad.nocknock.utilities.livedata.map
import com.afollestad.nocknock.utilities.livedata.zip
import com.afollestad.nocknock.utilities.providers.StringProvider
import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -54,7 +54,7 @@ class ViewSiteViewModel(
private val stringProvider: StringProvider,
private val database: AppDatabase,
private val notificationManager: NockNotificationManager,
private val validationManager: ValidationExecutor,
private val validationManager: ValidationManager,
mainDispatcher: CoroutineDispatcher,
private val ioDispatcher: CoroutineDispatcher
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
@ -64,7 +64,6 @@ class ViewSiteViewModel(
// Public properties
val status = MutableLiveData<Status>()
val name = MutableLiveData<String>()
val tags = MutableLiveData<String>()
val url = MutableLiveData<String>()
val timeout = MutableLiveData<Int>()
val validationMode = MutableLiveData<ValidationMode>()
@ -74,15 +73,25 @@ class ViewSiteViewModel(
val checkIntervalUnit = MutableLiveData<Long>()
val retryPolicyTimes = MutableLiveData<Int>()
val retryPolicyMinutes = MutableLiveData<Int>()
val headers = MutableLiveData<List<Header>>()
val certificateUri = MutableLiveData<String>()
internal val disabled = MutableLiveData<Boolean>()
internal val lastResult = MutableLiveData<ValidationResult?>()
// Private properties
private val isLoading = MutableLiveData<Boolean>()
private val nameError = MutableLiveData<Int?>()
private val urlError = MutableLiveData<Int?>()
private val timeoutError = MutableLiveData<Int?>()
private val validationSearchTermError = MutableLiveData<Int?>()
private val validationScriptError = MutableLiveData<Int?>()
private val checkIntervalValueError = MutableLiveData<Int?>()
// Expose private properties or calculated properties
@CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading
@CheckResult fun onNameError(): LiveData<Int?> = nameError
@CheckResult fun onUrlError(): LiveData<Int?> = urlError
@CheckResult fun onUrlWarningVisibility(): LiveData<Boolean> {
return url.map {
val parsed = HttpUrl.parse(it)
@ -90,6 +99,8 @@ class ViewSiteViewModel(
}
}
@CheckResult fun onTimeoutError(): LiveData<Int?> = timeoutError
@CheckResult fun onValidationModeDescription(): LiveData<Int> {
return validationMode.map {
when (it!!) {
@ -100,11 +111,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> =
disabled.map {
@ -148,7 +168,7 @@ class ViewSiteViewModel(
withContext(ioDispatcher) {
database.updateSite(updatedModel)
}
validationManager.scheduleValidation(
validationManager.scheduleCheck(
site = updatedModel,
rightNow = true,
cancelPrevious = true
@ -164,7 +184,7 @@ class ViewSiteViewModel(
status = WAITING
)
setModel(checkModel)
validationManager.scheduleValidation(
validationManager.scheduleCheck(
site = checkModel,
rightNow = true,
cancelPrevious = true
@ -172,7 +192,7 @@ class ViewSiteViewModel(
}
fun removeSite(done: () -> Unit) {
validationManager.cancelScheduledValidation(site)
validationManager.cancelCheck(site)
notificationManager.cancelStatusNotification(site)
scope.launch {
@ -186,7 +206,7 @@ class ViewSiteViewModel(
}
fun disableSite() {
validationManager.cancelScheduledValidation(site)
validationManager.cancelCheck(site)
notificationManager.cancelStatusNotification(site)
scope.launch {
@ -222,16 +242,75 @@ class ViewSiteViewModel(
}
private fun getUpdatedDbModel(): Site? {
val timeout = timeout.value ?: 10_000
val cleanedTags = tags.value?.split(',')?.joinToString(separator = ",") ?: ""
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
if (timeout.value.isNullOrLessThan(1)) {
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 newSettings = site.settings!!.copy(
validationIntervalMs = getCheckIntervalMs(),
validationMode = validationMode.value!!,
validationArgs = getValidationArgs(),
networkTimeout = timeout,
disabled = false,
certificate = certificateUri.value?.toString()
networkTimeout = timeout.value!!,
disabled = false
)
val retryPolicyTimes = retryPolicyTimes.value ?: 0
@ -239,16 +318,10 @@ class ViewSiteViewModel(
val retryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) {
if (site.retryPolicy != null) {
// Have existing policy, update it
site.retryPolicy!!.copy(
count = retryPolicyTimes,
minutes = retryPolicyMinutes
)
site.retryPolicy!!.copy(count = retryPolicyTimes, minutes = retryPolicyMinutes)
} else {
// Create new policy
RetryPolicy(
count = retryPolicyTimes,
minutes = retryPolicyMinutes
)
RetryPolicy(count = retryPolicyTimes, minutes = retryPolicyMinutes)
}
} else {
// No policy
@ -257,11 +330,9 @@ class ViewSiteViewModel(
return site.copy(
name = name.value!!.trim(),
tags = cleanedTags,
url = url.value!!.trim(),
settings = newSettings,
retryPolicy = retryPolicy,
headers = headers.value ?: emptyList()
retryPolicy = retryPolicy
)
.withStatus(status = WAITING)
}

View file

@ -15,7 +15,7 @@
*/
package com.afollestad.nocknock.ui.viewsite
import com.afollestad.nocknock.data.model.RetryPolicy
import com.afollestad.nocknock.data.RetryPolicy
import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.data.model.Status.WAITING
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
@ -32,7 +32,6 @@ fun ViewSiteViewModel.setModel(site: Site) {
status.value = site.lastResult?.status ?: WAITING
name.value = site.name
tags.value = site.tags
url.value = site.url
timeout.value = settings.networkTimeout
@ -54,12 +53,6 @@ fun ViewSiteViewModel.setModel(site: Site) {
setCheckInterval(settings.validationIntervalMs)
setRetryPolicy(site.retryPolicy)
headers.value = site.headers
if (settings.certificate == "null") {
certificateUri.value = ""
} else {
certificateUri.value = settings.certificate
}
this.disabled.value = settings.disabled
this.lastResult.value = site.lastResult
@ -69,22 +62,22 @@ private fun ViewSiteViewModel.setCheckInterval(interval: Long) {
when {
interval >= WEEK -> {
checkIntervalValue.value =
getIntervalFromUnit(interval, WEEK)
getIntervalFromUnit(interval, WEEK)
checkIntervalUnit.value = WEEK
}
interval >= DAY -> {
checkIntervalValue.value =
getIntervalFromUnit(interval, DAY)
getIntervalFromUnit(interval, DAY)
checkIntervalUnit.value = DAY
}
interval >= HOUR -> {
checkIntervalValue.value =
getIntervalFromUnit(interval, HOUR)
getIntervalFromUnit(interval, HOUR)
checkIntervalUnit.value = HOUR
}
interval >= MINUTE -> {
checkIntervalValue.value =
getIntervalFromUnit(interval, MINUTE)
getIntervalFromUnit(interval, MINUTE)
checkIntervalUnit.value = MINUTE
}
else -> {

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorAccent" android:state_pressed="false"/>
<item android:color="#FFFFFF" android:state_pressed="true"/>
</selector>

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?colorAccent"/>
<stroke
android:color="@color/colorAccent_pressed"
android:width="1dp"/>
<corners android:radius="6dp"/>
<padding
android:bottom="12dp"
android:left="12dp"
android:right="12dp"
android:top="12dp"/>
</shape>

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/colorAccent_pressed"/>
<stroke
android:color="?colorAccent"
android:width="1dp"/>
<corners android:radius="6dp"/>
<padding
android:bottom="12dp"
android:left="12dp"
android:right="12dp"
android:top="12dp"/>
</shape>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/checked_chip" android:state_pressed="false"/>
<item android:drawable="@drawable/checked_chip_pressed" android:state_pressed="true"/>
</selector>

View file

@ -1,10 +0,0 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="24dp">
<path
android:fillColor="?iconColor"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?android:windowBackground"/>
<stroke
android:color="?colorAccent"
android:width="1dp"/>
<corners android:radius="6dp"/>
<padding
android:bottom="12dp"
android:left="12dp"
android:right="12dp"
android:top="12dp"/>
</shape>

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/colorAccent_translucent"/>
<stroke
android:color="?colorAccent"
android:width="1dp"/>
<corners android:radius="6dp"/>
<padding
android:bottom="12dp"
android:left="12dp"
android:right="12dp"
android:top="12dp"/>
</shape>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/unchecked_chip" android:state_pressed="false"/>
<item android:drawable="@drawable/unchecked_chip_pressed" android:state_pressed="true"/>
</selector>

View file

@ -16,7 +16,6 @@
<include layout="@layout/include_app_bar"/>
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
@ -25,61 +24,59 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/content_inset_double"
android:paddingBottom="@dimen/content_inset"
android:paddingLeft="@dimen/content_inset"
android:paddingRight="@dimen/content_inset"
android:paddingTop="@dimen/content_inset_half"
>
<TextView
android:layout_marginTop="0dp"
android:text="@string/site_name"
style="@style/InputForm.Header"
/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/nameTiLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="-4dp"
android:layout_marginRight="-4dp"
android:layout_marginTop="@dimen/content_inset"
>
<EditText
android:id="@+id/inputName"
android:hint="@string/site_name_hint"
android:inputType="textPersonName|textCapWords|textAutoCorrect"
android:nextFocusDown="@+id/inputUrl"
tools:ignore="Autofill"
style="@style/InputForm.Field"
/>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/inputName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/site_name"
android:inputType="textPersonName|textCapWords|textAutoCorrect"
style="@style/NockText.Body"
/>
<TextView
android:text="@string/site_url"
style="@style/InputForm.Header"
/>
</com.google.android.material.textfield.TextInputLayout>
<EditText
android:id="@+id/inputUrl"
android:hint="@string/site_url_hint"
android:inputType="textUri"
android:nextFocusDown="@+id/inputTags"
tools:ignore="Autofill"
style="@style/InputForm.Field"
/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/urlTiLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="-4dp"
android:layout_marginRight="-4dp"
android:layout_marginTop="@dimen/content_inset_half"
>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/inputUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/site_url"
android:inputType="textUri"
style="@style/NockText.Body"
/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/textUrlWarning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/list_text_spacing"
android:text="@string/warning_http_url"
android:visibility="gone"
style="@style/InputForm.FieldNote"
/>
<TextView
android:text="@string/site_tags"
style="@style/InputForm.Header"
/>
<EditText
android:id="@+id/inputTags"
android:digits=",.?-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ "
android:hint="@string/site_tags_hint"
android:inputType="text|textCapWords"
android:nextFocusDown="@+id/inputUrl"
tools:ignore="Autofill"
style="@style/InputForm.Field"
style="@style/NockText.Footnote"
/>
<include layout="@layout/include_divider"/>
@ -91,10 +88,35 @@
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"
/>
<TextView
android:id="@+id/responseValidationLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/content_inset_more"
android:text="@string/response_validation_mode"
style="@style/InputForm.Header"
style="@style/NockText.SectionHeader"
/>
<Spinner
@ -123,7 +145,7 @@
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/content_inset"
android:layout_marginTop="@dimen/content_inset_half"
android:background="?scriptLayoutBackground"
android:background="@color/lighterGray"
/>
<TextView
@ -136,8 +158,6 @@
style="@style/NockText.Body.Light"
/>
<include layout="@layout/include_divider"/>
<com.afollestad.nocknock.viewcomponents.retrypolicy.RetryPolicyLayout
android:id="@+id/retryPolicyLayout"
android:layout_width="match_parent"
@ -145,66 +165,13 @@
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
<com.google.android.material.button.MaterialButton
android:id="@+id/doneBtn"
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"/>
<com.afollestad.nocknock.viewcomponents.headers.HeaderStackLayout
android:id="@+id/headersLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/content_inset_half"
android:layout_marginTop="@dimen/content_inset_double"
android:text="@string/add_site"
style="@style/AccentButton"
/>
</LinearLayout>

View file

@ -17,19 +17,6 @@
<include layout="@layout/include_app_bar"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tags_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingBottom="@dimen/content_inset_half"
android:paddingEnd="@dimen/content_inset"
android:paddingStart="@dimen/content_inset"
android:paddingTop="@dimen/content_inset_half"
android:scrollbars="none"
android:visibility="gone"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"

View file

@ -26,30 +26,15 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/content_inset_double"
android:paddingBottom="@dimen/content_inset"
android:paddingLeft="@dimen/content_inset"
android:paddingRight="@dimen/content_inset"
android:paddingTop="@dimen/content_inset_less"
android:paddingTop="@dimen/content_inset"
>
<EditText
android:id="@+id/inputName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:hint="@string/site_name"
android:inputType="textPersonName|textCapWords|textAutoCorrect"
android:nextFocusDown="@+id/inputUrl"
android:singleLine="true"
android:transitionName="site_name"
tools:ignore="Autofill,UnusedAttribute"
style="@style/NockText.Header"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/content_inset_quarter"
android:orientation="horizontal"
>
@ -70,13 +55,24 @@
android:orientation="vertical"
>
<EditText
android:id="@+id/inputName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/site_name"
android:inputType="textPersonName|textCapWords|textAutoCorrect"
android:singleLine="true"
android:transitionName="site_name"
tools:ignore="Autofill,UnusedAttribute"
style="@style/NockText.Body"
/>
<EditText
android:id="@+id/inputUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/site_url"
android:inputType="textUri"
android:nextFocusDown="@+id/inputTags"
android:singleLine="true"
android:transitionName="site_url"
tools:ignore="Autofill,UnusedAttribute"
@ -95,19 +91,6 @@
style="@style/NockText.Footnote"
/>
<EditText
android:id="@+id/inputTags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:digits=",.?-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ "
android:hint="@string/site_tags_hint"
android:imeOptions="actionNext"
android:inputType="text|textCapWords"
android:singleLine="true"
tools:ignore="Autofill,UnusedAttribute"
style="@style/NockText.Body"
/>
</LinearLayout>
</LinearLayout>
@ -126,6 +109,35 @@
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:hint="@string/response_timeout_default"
android:inputType="number"
android:layout_marginTop="@dimen/content_inset_quarter"
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
android:id="@+id/responseValidationLabel"
android:layout_width="wrap_content"
@ -161,7 +173,7 @@
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/content_inset"
android:layout_marginTop="@dimen/content_inset_half"
android:background="?scriptLayoutBackground"
android:background="@color/lighterGray"
/>
<TextView
@ -174,13 +186,6 @@
style="@style/NockText.Body.Light"
/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="@dimen/content_inset_less"
android:background="?dividerColor"
/>
<com.afollestad.nocknock.viewcomponents.retrypolicy.RetryPolicyLayout
android:id="@+id/retryPolicyLayout"
android:layout_width="match_parent"
@ -188,76 +193,6 @@
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"/>
<com.afollestad.nocknock.viewcomponents.headers.HeaderStackLayout
android:id="@+id/headersLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/content_inset_half"
/>
<include layout="@layout/include_divider"/>
<TextView
@ -294,6 +229,24 @@
style="@style/NockText.Body"
/>
<com.google.android.material.button.MaterialButton
android:id="@+id/doneBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/content_inset_double"
android:text="@string/save_changes"
style="@style/AccentButton"
/>
<com.google.android.material.button.MaterialButton
android:id="@+id/disableChecksButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/content_inset_quarter"
android:text="@string/disable_automatic_checks"
style="@style/PrimaryDarkButton"
/>
</LinearLayout>
</ScrollView>

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.AppCompatTextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/chip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/content_inset_half"
android:background="@drawable/unchecked_chip_selector"
android:textColor="?colorAccent"
app:textAllCaps="true"
tools:text="Testing"
style="@style/NockText.Body"
/>

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/commit"
android:icon="@drawable/ic_check"
android:title="@string/add_site"
app:showAsAction="ifRoom"/>
</menu>

View file

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

View file

@ -1,23 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/commit"
android:icon="@drawable/ic_check"
android:title="@string/save_changes"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/refresh"
android:icon="@drawable/ic_action_refresh"
android:title="@string/refresh_status"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/remove"
android:icon="@drawable/ic_action_delete"
android:title="@string/remove_site"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/disableChecks"
android:title="@string/disable_automatic_checks"
/>
</menu>

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -3,7 +3,6 @@
<string-array name="site_long_options" translatable="false">
<item>@string/refresh_status</item>
<item>@string/duplicate_and_modify</item>
<item>@string/remove_site</item>
</string-array>
@ -13,4 +12,10 @@
<item>JavaScript Evaluation</item>
</string-array>
<string-array name="donation_options">
<item>via PayPal</item>
<item>via Cash App</item>
<item>via Venmo</item>
</string-array>
</resources>

View file

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

View file

@ -7,11 +7,7 @@
<color name="colorPrimary_darkTheme">#212121</color>
<color name="colorPrimaryDark_darkTheme">#252525</color>
<color name="darkerGray">#303030</color>
<color name="lighterGray">#EEEEEE</color>
<color name="lighterGray">#303030</color>
<color name="colorAccent">#FF6E40</color>
<color name="colorAccent_pressed">#E44615</color>
<color name="colorAccent_translucent">#40FF6E40</color>
</resources>

View file

@ -2,6 +2,5 @@
<dimen name="empty_text_size">28sp</dimen>
<dimen name="list_text_spacing">6dp</dimen>
<dimen name="toolbar_elevation">4dp</dimen>
</resources>

View file

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

View file

@ -1,42 +1,35 @@
<resources>
<string name="app_name">Nock Nock</string>
<string name="app_name_x">Nock Nock %1$s</string>
<string name="no_sites_added">No sites added!</string>
<string name="about">About</string>
<string name="about_body"><![CDATA[
A simple app designed by <b>Aidan Follestad</b>.<br/>
<b>Nock Nock</b>, a simple app designed by <b>Aidan Follestad</b>.<br/>
<a href=\'https://af.codes\'>Website</a>&nbsp;&nbsp;
<a href=\'https://twitter.com/afollestad\'>Twitter</a>&nbsp;&nbsp;
<a href=\'https://github.com/afollestad\'>GitHub</a>&nbsp;&nbsp;
<a href=\'https://www.linkedin.com/in/afollestad\'>LinkedIn</a>
<br/><br/><i>Nock Nock is open source! Check out the <a href=\'https://github.com/afollestad/nock-nock\'>GitHub page</a>!</i>
<br/>Icon by <a href=\'https://plus.google.com/+KevinAguilarC\'>Kevin Aguilar</a> of <b>221 Pixels</b>.
<br/>View the <a href=\'https://af.codes/privacypolicies/nocknock.html\'>Privacy Policy</a>.
]]></string>
<string name="dark_mode">Dark Mode</string>
<string name="dismiss">Dismiss</string>
<string name="add_site">Add Site</string>
<string name="site_name">Site Name</string>
<string name="site_name_hint">Site display name</string>
<string name="site_url">Site URL</string>
<string name="site_url_hint">https://yoursite.com</string>
<string name="site_tags">Site Tags</string>
<string name="site_tags_hint">e.g. One,Two,Three</string>
<string name="site_tags_hint_full">Tags (e.g. One,Two,Three)</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_valid_url">Please enter a valid URL.</string>
<string name="please_enter_check_interval">Please input a validation interval.</string>
<string name="please_enter_search_term">Please input a search term.</string>
<string name="please_enter_javaScript">Please input a validation script.</string>
<string name="please_enter_networkTimeout">Please enter a network timeout greater than 0.</string>
<string name="please_enter_validCertUri">Certificate should be a valid file or content URI.</string>
<string name="options">Options</string>
<string name="remove_site">Remove Site</string>
<string name="duplicate_and_modify">Duplicate and Modify</string>
<string name="remove_site_prompt"><![CDATA[Remove <b>%1$s</b> from your sites?]]></string>
<string name="remove">Remove</string>
<string name="save_changes">Save Changes</string>
@ -50,8 +43,8 @@
<string name="disable_automatic_checks">Disable Automatic Validation</string>
<string name="disable_automatic_checks_prompt"><![CDATA[
Disable automatic validation for <b>%1$s</b>? The site will not be checked in the background
until you re-enable validation by tapping the checkmark (Save) icon. You can still manually
perform validation by tapping the Refresh icon at the top of this page.
until you re-enable validation for it. You can still manually perform validation by tapping the
Refresh icon at the top of this page.
]]></string>
<string name="disable">Disable</string>
<string name="renable_and_save_changes">Enable Auto Validation &amp; Save Changes</string>
@ -59,10 +52,6 @@
<string name="response_timeout">Network Response Timeout (ms)</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="warning_http_url">
@ -85,6 +74,14 @@
exception to pass custom error messages to Nock Nock.
</string>
<string name="support_me">Donate</string>
<string name="support_me_message"><![CDATA[
<b>Nock Nock</b> was created and is maintained by one person. Donations are <b>much</b>
appreciated and encourage continued support.
]]></string>
<string name="thank_you">Thank you very much!</string>
<string name="next">Next</string>
<string name="install_web_browser">Please install a web browser app, such as Google Chrome.</string>
</resources>

View file

@ -4,9 +4,15 @@
<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">
<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>

View file

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

View file

@ -6,28 +6,4 @@
<item name="android:textColor">?toolbarTitleColor</item>
</style>
<style name="InputForm"/>
<style name="InputForm.Header" parent="NockText.SectionHeader">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginTop">@dimen/content_inset_less</item>
</style>
<style name="InputForm.Field" parent="NockText.Body">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginTop">@dimen/content_inset_quarter</item>
<item name="android:singleLine">true</item>
<item name="android:imeOptions">actionNext</item>
<item name="android:layout_marginStart">-4dp</item>
<item name="android:layout_marginEnd">-4dp</item>
</style>
<style name="InputForm.FieldNote" parent="NockText.Footnote">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginTop">@dimen/list_text_spacing</item>
</style>
</resources>

View file

@ -19,13 +19,11 @@ import android.app.PendingIntent
import android.content.Intent
import android.content.IntentFilter
import com.afollestad.nocknock.data.AppDatabase
import com.afollestad.nocknock.data.HeaderDao
import com.afollestad.nocknock.data.RetryPolicy
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
@ -57,8 +55,7 @@ fun fakeSettingsModel(
validationMode = validationMode,
validationArgs = null,
disabled = false,
networkTimeout = 10000,
certificate = null
networkTimeout = 10000
)
fun fakeResultModel(
@ -82,31 +79,18 @@ fun fakeRetryPolicy(
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,
tags: String = ""
) = Site(
fun fakeModel(id: Long) = Site(
id = id,
name = "Test",
url = "https://test.com",
tags = tags,
settings = fakeSettingsModel(id),
lastResult = fakeResultModel(id),
retryPolicy = fakeRetryPolicy(id),
headers = fakeHeaders(id)
retryPolicy = fakeRetryPolicy(id)
)
val MOCK_MODEL_1 = fakeModel(1, tags = "one,two")
val MOCK_MODEL_2 = fakeModel(2, tags = "three,four")
val MOCK_MODEL_3 = fakeModel(3, tags = "five,six")
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 {
@ -171,29 +155,12 @@ fun mockDatabase(): AppDatabase {
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
}
}

View file

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

View file

@ -20,7 +20,7 @@ import com.afollestad.nocknock.ALL_MOCK_MODELS
import com.afollestad.nocknock.MOCK_MODEL_1
import com.afollestad.nocknock.MOCK_MODEL_2
import com.afollestad.nocknock.MOCK_MODEL_3
import com.afollestad.nocknock.engine.validation.ValidationExecutor
import com.afollestad.nocknock.engine.validation.ValidationManager
import com.afollestad.nocknock.mockDatabase
import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.utilities.livedata.test
@ -39,7 +39,7 @@ class MainViewModelTest {
private val database = mockDatabase()
private val notificationManager = mock<NockNotificationManager>()
private val validationManager = mock<ValidationExecutor>()
private val validationManager = mock<ValidationManager>()
@Rule @JvmField val rule = InstantTaskExecutorRule()
@ -60,45 +60,18 @@ class MainViewModelTest {
.test()
val sites = viewModel.onSites()
.test()
val tags = viewModel.onTags()
.test()
val tagsVisibility = viewModel.onTagsListVisibility()
.test()
viewModel.onResume()
verify(notificationManager).cancelStatusNotifications()
verify(validationManager).ensureScheduledValidations()
verify(validationManager).ensureScheduledChecks()
sites.assertValues(ALL_MOCK_MODELS)
sites.assertValues(
listOf(),
ALL_MOCK_MODELS
)
isLoading.assertValues(true, 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() {
@ -113,7 +86,10 @@ class MainViewModelTest {
.test()
viewModel.onResume()
sites.assertValues(ALL_MOCK_MODELS)
sites.assertValues(
listOf(),
ALL_MOCK_MODELS
)
val updatedModel2 = MOCK_MODEL_2.copy(
name = "Wakanda Forever!!!"
@ -130,7 +106,7 @@ class MainViewModelTest {
@Test fun refreshSite() {
viewModel.refreshSite(MOCK_MODEL_3)
verify(validationManager).scheduleValidation(
verify(validationManager).scheduleCheck(
site = MOCK_MODEL_3,
rightNow = true,
cancelPrevious = true
@ -144,7 +120,10 @@ class MainViewModelTest {
.test()
viewModel.onResume()
sites.assertValues(ALL_MOCK_MODELS)
sites.assertValues(
listOf(),
ALL_MOCK_MODELS
)
isLoading.assertValues(true, false)
val modifiedModel = MOCK_MODEL_1.copy(id = 11111)
@ -153,7 +132,7 @@ class MainViewModelTest {
sites.assertNoValues()
isLoading.assertValues(true, false)
verify(validationManager).cancelScheduledValidation(modifiedModel)
verify(validationManager).cancelCheck(modifiedModel)
verify(notificationManager).cancelStatusNotification(modifiedModel)
verify(database.siteDao()).delete(modifiedModel)
verify(database.siteSettingsDao()).delete(modifiedModel.settings!!)
@ -168,7 +147,10 @@ class MainViewModelTest {
.test()
viewModel.onResume()
sites.assertValues(ALL_MOCK_MODELS)
sites.assertValues(
listOf(),
ALL_MOCK_MODELS
)
isLoading.assertValues(true, false)
val modelsWithout1 = ALL_MOCK_MODELS.toMutableList()
@ -181,7 +163,7 @@ class MainViewModelTest {
isLoading.assertValues(true, false)
emptyTextVisibility.assertValues(false, false, false)
verify(validationManager).cancelScheduledValidation(MOCK_MODEL_1)
verify(validationManager).cancelCheck(MOCK_MODEL_1)
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
verify(database.siteDao()).delete(MOCK_MODEL_1)
verify(database.siteSettingsDao()).delete(MOCK_MODEL_1.settings!!)

View file

@ -18,8 +18,6 @@ package com.afollestad.nocknock.ui.viewsite
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.afollestad.nocknock.MOCK_MODEL_1
import com.afollestad.nocknock.R
import com.afollestad.nocknock.data.model.Header
import com.afollestad.nocknock.data.model.RetryPolicy
import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.data.model.SiteSettings
import com.afollestad.nocknock.data.model.Status.CHECKING
@ -30,8 +28,7 @@ import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
import com.afollestad.nocknock.data.model.ValidationResult
import com.afollestad.nocknock.engine.validation.ValidationExecutor
import com.afollestad.nocknock.fakeRetryPolicy
import com.afollestad.nocknock.engine.validation.ValidationManager
import com.afollestad.nocknock.mockDatabase
import com.afollestad.nocknock.notifications.NockNotificationManager
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.argumentCaptor
import com.nhaarman.mockitokotlin2.doAnswer
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
@ -79,7 +75,7 @@ class ViewSiteViewModelTest {
}
}
private val database = mockDatabase()
private val validationManager = mock<ValidationExecutor>()
private val validationManager = mock<ValidationManager>()
private val notificationManager = mock<NockNotificationManager>()
@Rule @JvmField val rule = InstantTaskExecutorRule()
@ -259,11 +255,247 @@ class ViewSiteViewModelTest {
.isEqualTo("Two")
}
@Test fun commit_success() = runBlocking {
whenever(database.retryPolicyDao().forSite(any())).doReturn(listOf(fakeRetryPolicy(1)))
@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())
.scheduleCheck(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())
.scheduleCheck(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())
.scheduleCheck(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())
.scheduleCheck(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())
.scheduleCheck(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())
.scheduleCheck(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())
.scheduleCheck(any(), any(), any(), any())
onNameError.assertNoValues()
onUrlError.assertNoValues()
onTimeoutError.assertNoValues()
onCheckIntervalError.assertNoValues()
onSearchTermError.assertNoValues()
onScriptError.assertValues(R.string.please_enter_javaScript)
verify(onDone, never()).invoke()
}
@Test fun commit_success() = runBlocking {
val isLoading = viewModel.onIsLoading()
.test()
val onNameError = viewModel.onNameError()
.test()
val onUrlError = viewModel.onUrlError()
.test()
val onTimeoutError = viewModel.onTimeoutError()
.test()
val onSearchTermError = viewModel.onValidationSearchTermError()
.test()
val onScriptError = viewModel.onValidationScriptError()
.test()
val onCheckIntervalError = viewModel.onCheckIntervalError()
.test()
fillInModel()
val onDone = mock<() -> Unit>()
@ -274,13 +506,11 @@ class ViewSiteViewModelTest {
val siteCaptor = argumentCaptor<Site>()
val settingsCaptor = argumentCaptor<SiteSettings>()
val resultCaptor = argumentCaptor<ValidationResult>()
val retryPolicyCaptor = argumentCaptor<RetryPolicy>()
isLoading.assertValues(true, false)
verify(database.siteDao()).update(siteCaptor.capture())
verify(database.siteSettingsDao()).update(settingsCaptor.capture())
verify(database.validationResultsDao()).update(resultCaptor.capture())
verify(database.retryPolicyDao()).update(retryPolicyCaptor.capture())
// From fillInModel() below
val updatedSettings = MOCK_MODEL_1.settings!!.copy(
@ -293,26 +523,31 @@ class ViewSiteViewModelTest {
val updatedResult = MOCK_MODEL_1.lastResult!!.copy(
status = WAITING
)
val retryPolicy = retryPolicyCaptor.firstValue
val updatedModel = MOCK_MODEL_1.copy(
name = "Hello There",
url = "https://www.hellothere.com",
settings = updatedSettings,
lastResult = updatedResult,
retryPolicy = retryPolicy
lastResult = updatedResult
)
assertThat(siteCaptor.firstValue).isEqualTo(updatedModel)
assertThat(settingsCaptor.firstValue).isEqualTo(updatedSettings)
assertThat(resultCaptor.firstValue).isEqualTo(updatedResult)
verify(validationManager).scheduleValidation(
verify(validationManager).scheduleCheck(
site = updatedModel,
rightNow = true,
cancelPrevious = true,
fromFinishingJob = false
)
onNameError.assertNoValues()
onUrlError.assertNoValues()
onTimeoutError.assertNoValues()
onCheckIntervalError.assertNoValues()
onSearchTermError.assertNoValues()
onScriptError.assertNoValues()
verify(onDone).invoke()
}
@ -327,7 +562,7 @@ class ViewSiteViewModelTest {
)
viewModel.checkNow()
verify(validationManager).scheduleValidation(
verify(validationManager).scheduleCheck(
site = expectedModel,
rightNow = true,
cancelPrevious = true
@ -344,7 +579,7 @@ class ViewSiteViewModelTest {
viewModel.removeSite(onDone)
isLoading.assertValues(true, false)
verify(validationManager).cancelScheduledValidation(MOCK_MODEL_1)
verify(validationManager).cancelCheck(MOCK_MODEL_1)
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
verify(database.siteDao()).delete(MOCK_MODEL_1)
verify(database.siteSettingsDao()).delete(MOCK_MODEL_1.settings!!)
@ -368,7 +603,7 @@ class ViewSiteViewModelTest {
)
)
verify(validationManager).cancelScheduledValidation(MOCK_MODEL_1)
verify(validationManager).cancelCheck(MOCK_MODEL_1)
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
verify(database.siteDao()).update(expectedSite)
verify(database.siteSettingsDao()).update(expectedSite.settings!!)
@ -384,12 +619,5 @@ class ViewSiteViewModelTest {
validationScript.value = "throw 'Oh no!'"
checkIntervalValue.value = 24
checkIntervalUnit.value = 60000
tags.value = "one,two"
retryPolicyTimes.value = 5
retryPolicyMinutes.value = 5
headers.value = listOf(
Header(2L, 1L, key = "Content-Type", value = "text/html"),
Header(3L, 1L, key = "User-Agent", value = "NockNock")
)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 528 KiB

BIN
art/showcasemain3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 KiB

View file

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

View file

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

View file

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

View file

@ -21,12 +21,7 @@ import android.widget.EditText
import androidx.annotation.IntRange
import kotlin.math.min
fun EditText.setTextAndMaintainSelection(text: CharSequence?) {
if (text == null) {
setText("")
return
}
fun EditText.setTextAndMaintainSelection(text: CharSequence) {
val formerStart = min(selectionStart, text.length)
val formerEnd = min(selectionEnd, text.length)
setText(text)

View file

@ -30,8 +30,6 @@ interface CanNotifyModel : Serializable {
fun notifyName(): String
fun notifyTag(): String
fun notifyDescription(): String?
}
/** @author Aidan Follestad (@afollestad) */

View file

@ -20,7 +20,6 @@ import android.app.PendingIntent
import android.content.Context
import android.graphics.Bitmap
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.BigTextStyle
import androidx.core.app.NotificationCompat.DEFAULT_VIBRATE
/** @author Aidan Follestad (@afollestad) */
@ -57,10 +56,6 @@ class RealNotificationProvider(
.setLargeIcon(largeIcon)
.setAutoCancel(true)
.setDefaults(DEFAULT_VIBRATE)
.setStyle(
BigTextStyle()
.bigText(content)
)
.build()
}
}

View file

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

View file

@ -4,7 +4,7 @@
<application>
<uses-library
android:name="androidx.test.runner"
android:name="android.test.runner"
android:required="false"/>
</application>
</manifest>

View file

@ -21,8 +21,6 @@ import android.content.Context
import androidx.room.Room.inMemoryDatabaseBuilder
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.runner.AndroidJUnit4
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.ERROR
@ -48,7 +46,6 @@ class AppDatabaseTest() {
private lateinit var settingsDao: SiteSettingsDao
private lateinit var resultsDao: ValidationResultsDao
private lateinit var retryDao: RetryPolicyDao
private lateinit var headerDao: HeaderDao
@Before fun setup() {
val context = getApplicationContext<Context>()
@ -57,12 +54,13 @@ class AppDatabaseTest() {
settingsDao = db.siteSettingsDao()
resultsDao = db.validationResultsDao()
retryDao = db.retryPolicyDao()
headerDao = db.headerDao()
}
@After
@Throws(IOException::class)
fun destroy() = db.close()
fun destroy() {
db.close()
}
// SiteDao
@ -70,11 +68,9 @@ class AppDatabaseTest() {
val model1 = Site(
name = "Test 1",
url = "https://test1.com",
tags = "",
settings = null,
lastResult = null,
retryPolicy = null,
headers = emptyList()
retryPolicy = null
)
val newId1 = sitesDao.insert(model1)
assertThat(newId1).isGreaterThan(0)
@ -82,11 +78,9 @@ class AppDatabaseTest() {
val model2 = Site(
name = "Test 2",
url = "https://test2.com",
tags = "",
settings = null,
lastResult = null,
retryPolicy = null,
headers = emptyList()
retryPolicy = null
)
val newId2 = sitesDao.insert(model2)
assertThat(newId2).isGreaterThan(newId1)
@ -101,11 +95,9 @@ class AppDatabaseTest() {
val model = Site(
name = "Test",
url = "https://test.com",
tags = "",
settings = null,
lastResult = null,
retryPolicy = null,
headers = emptyList()
retryPolicy = null
)
val newId = sitesDao.insert(model)
assertThat(newId).isGreaterThan(0)
@ -118,11 +110,9 @@ class AppDatabaseTest() {
val initialModel = Site(
name = "Test 1",
url = "https://test1.com",
tags = "",
settings = null,
lastResult = null,
retryPolicy = null,
headers = emptyList()
retryPolicy = null
)
val newId = sitesDao.insert(initialModel)
assertThat(newId).isGreaterThan(0)
@ -144,11 +134,9 @@ class AppDatabaseTest() {
val model1 = Site(
name = "Test 1",
url = "https://test1.com",
tags = "",
settings = null,
lastResult = null,
retryPolicy = null,
headers = emptyList()
retryPolicy = null
)
val newId1 = sitesDao.insert(model1)
assertThat(newId1).isGreaterThan(0)
@ -156,11 +144,9 @@ class AppDatabaseTest() {
val model2 = Site(
name = "Test 2",
url = "https://test2.com",
tags = "",
settings = null,
lastResult = null,
retryPolicy = null,
headers = emptyList()
retryPolicy = null
)
val newId2 = sitesDao.insert(model2)
assertThat(newId2).isGreaterThan(newId1)
@ -181,8 +167,7 @@ class AppDatabaseTest() {
validationMode = STATUS_CODE,
validationArgs = null,
disabled = false,
networkTimeout = 10000,
certificate = null
networkTimeout = 10000
)
val newId = settingsDao.insert(model)
assertThat(newId).isEqualTo(1)
@ -200,8 +185,7 @@ class AppDatabaseTest() {
validationMode = STATUS_CODE,
validationArgs = null,
disabled = false,
networkTimeout = 10000,
certificate = null
networkTimeout = 10000
)
)
@ -229,8 +213,7 @@ class AppDatabaseTest() {
validationMode = STATUS_CODE,
validationArgs = null,
disabled = false,
networkTimeout = 10000,
certificate = null
networkTimeout = 10000
)
)
@ -308,7 +291,7 @@ class AppDatabaseTest() {
val newId = retryDao.insert(model)
assertThat(newId).isEqualTo(1)
val finalModel = retryDao.forSite(newId)
val finalModel = resultsDao.forSite(newId)
.single()
assertThat(finalModel).isEqualTo(model.copy(siteId = newId))
}
@ -350,78 +333,6 @@ class AppDatabaseTest() {
assertThat(retryDao.forSite(1)).isEmpty()
}
// HeaderDao
@Test fun headers_insert_and_forSite() {
val models = listOf(
Header(
siteId = 1,
key = "Name",
value = "Aidan"
),
Header(
siteId = 1,
key = "Born",
value = "1995"
)
)
val newIds = headerDao.insert(models)
assertThat(newIds.first()).isEqualTo(1)
assertThat(newIds.last()).isEqualTo(2)
val finalModels = headerDao.forSite(1)
assertThat(finalModels.first()).isEqualTo(models.first().copy(id = 1))
assertThat(finalModels.last()).isEqualTo(models.last().copy(id = 2))
}
@Test fun headers_update() {
val models = listOf(
Header(
siteId = 1,
key = "Name",
value = "Aidan"
),
Header(
siteId = 1,
key = "Born",
value = "1995"
)
)
headerDao.insert(models)
val insertedModel = headerDao.forSite(1)
.last()
val updatedModel = insertedModel.copy(
key = "Test",
value = "Hello"
)
assertThat(headerDao.update(updatedModel)).isEqualTo(1)
val finalModels = headerDao.forSite(1)
assertThat(finalModels.first()).isEqualTo(models.first().copy(id = 1))
assertThat(finalModels.last()).isEqualTo(updatedModel)
}
@Test fun headers_delete() {
val models = listOf(
Header(
siteId = 1,
key = "Name",
value = "Aidan"
),
Header(
siteId = 1,
key = "Born",
value = "1995"
)
)
headerDao.insert(models)
val insertedModels = headerDao.forSite(1)
headerDao.delete(insertedModels)
assertThat(headerDao.forSite(1)).isEmpty()
}
// Extension Methods
@Test fun extension_put_and_allSites() {
@ -431,30 +342,9 @@ class AppDatabaseTest() {
val allSites = db.allSites()
assertThat(allSites.size).isEqualTo(3)
assertThat(allSites[0]).isEqualTo(
MOCK_MODEL_1.copy(
headers = listOf(
MOCK_MODEL_1.headers.first().copy(id = 1),
MOCK_MODEL_1.headers.last().copy(id = 2)
)
)
)
assertThat(allSites[1]).isEqualTo(
MOCK_MODEL_2.copy(
headers = listOf(
MOCK_MODEL_2.headers.first().copy(id = 3),
MOCK_MODEL_2.headers.last().copy(id = 4)
)
)
)
assertThat(allSites[2]).isEqualTo(
MOCK_MODEL_3.copy(
headers = listOf(
MOCK_MODEL_3.headers.first().copy(id = 5),
MOCK_MODEL_3.headers.last().copy(id = 6)
)
)
)
assertThat(allSites[0]).isEqualTo(MOCK_MODEL_1)
assertThat(allSites[1]).isEqualTo(MOCK_MODEL_2)
assertThat(allSites[2]).isEqualTo(MOCK_MODEL_3)
}
@Test fun extension_put_getSite() {
@ -489,25 +379,12 @@ class AppDatabaseTest() {
count = 4,
minutes = 8
)
val updatedHeaders = listOf(
modelToUpdate.headers.first().copy(
id = 7,
key = "One",
value = "Hello"
),
modelToUpdate.headers.last().copy(
id = 8,
key = "Two",
value = "Hey"
)
)
val updatedModel = modelToUpdate.copy(
name = "Oijrfouhef",
url = "https://iojfdfsdk.io",
settings = updatedSettings,
lastResult = updatedValidationResult,
retryPolicy = updatedRetryPolicy,
headers = updatedHeaders
retryPolicy = updatedRetryPolicy
)
db.updateSite(updatedModel)
@ -516,8 +393,6 @@ class AppDatabaseTest() {
assertThat(finalSite.settings).isEqualTo(updatedSettings)
assertThat(finalSite.lastResult).isEqualTo(updatedValidationResult)
assertThat(finalSite.retryPolicy).isEqualTo(updatedRetryPolicy)
assertThat(finalSite.headers.first()).isEqualTo(updatedHeaders.first())
assertThat(finalSite.headers.last()).isEqualTo(updatedHeaders.last())
assertThat(finalSite).isEqualTo(updatedModel)
}
@ -527,7 +402,7 @@ class AppDatabaseTest() {
db.putSite(MOCK_MODEL_3)
val allSites = db.allSites()
db.deleteSite(allSites[1])
db.deleteSite(MOCK_MODEL_2)
val remainingSettings = settingsDao.all()
assertThat(remainingSettings.size).isEqualTo(2)
@ -543,12 +418,5 @@ class AppDatabaseTest() {
assertThat(remainingRetryPolicies.size).isEqualTo(2)
assertThat(remainingRetryPolicies[0]).isEqualTo(allSites[0].retryPolicy!!)
assertThat(remainingRetryPolicies[1]).isEqualTo(allSites[2].retryPolicy!!)
val remainingHeaders = headerDao.all()
assertThat(remainingHeaders.size).isEqualTo(4)
assertThat(remainingHeaders[0]).isEqualTo(allSites[0].headers.first())
assertThat(remainingHeaders[1]).isEqualTo(allSites[0].headers.last())
assertThat(remainingHeaders[2]).isEqualTo(allSites[2].headers.first())
assertThat(remainingHeaders[3]).isEqualTo(allSites[2].headers.last())
}
}

View file

@ -15,8 +15,6 @@
*/
package com.afollestad.nocknock.data
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
@ -35,8 +33,7 @@ fun fakeSettingsModel(
validationMode = validationMode,
validationArgs = null,
disabled = false,
networkTimeout = 10000,
certificate = null
networkTimeout = 10000
)
fun fakeResultModel(
@ -60,20 +57,13 @@ fun fakeRetryPolicy(
minutes = minutes
)
fun fakeHeaders(siteId: Long) = listOf(
Header(siteId = siteId, key = "Content-Type", value = "text/html"),
Header(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)
retryPolicy = fakeRetryPolicy(id)
)
val MOCK_MODEL_1 = fakeModel(1)

View file

@ -19,8 +19,6 @@ import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.afollestad.nocknock.data.model.Converters
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.ValidationResult
@ -28,13 +26,12 @@ import com.afollestad.nocknock.data.model.ValidationResult
/** @author Aidan Follestad (@afollestad) */
@Database(
entities = [
Header::class,
RetryPolicy::class,
ValidationResult::class,
SiteSettings::class,
Site::class
],
version = 5,
version = 2,
exportSchema = false
)
@TypeConverters(Converters::class)
@ -47,8 +44,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun validationResultsDao(): ValidationResultsDao
abstract fun retryPolicyDao(): RetryPolicyDao
abstract fun headerDao(): HeaderDao
}
/**
@ -65,12 +60,10 @@ fun AppDatabase.allSites(): List<Site> {
.singleOrNull()
val retryPolicy = retryPolicyDao().forSite(it.id)
.singleOrNull()
val headers = headerDao().forSite(it.id)
return@map it.copy(
settings = settings,
lastResult = lastResult,
retryPolicy = retryPolicy,
headers = headers
retryPolicy = retryPolicy
)
}
}
@ -89,12 +82,10 @@ fun AppDatabase.getSite(id: Long): Site? {
.singleOrNull()
val retryPolicy = retryPolicyDao().forSite(id)
.singleOrNull()
val headers = headerDao().forSite(id)
return result.copy(
settings = settings,
lastResult = lastResult,
retryPolicy = retryPolicy,
headers = headers
retryPolicy = retryPolicy
)
}
@ -109,19 +100,14 @@ fun AppDatabase.putSite(site: Site): Site {
val settingsWithSiteId = settings.copy(siteId = newId)
val lastResultWithSiteId = site.lastResult?.copy(siteId = newId)
val retryPolicyWithSiteId = site.retryPolicy?.copy(siteId = newId)
val headersWithSiteId = site.headers.map { it.copy(siteId = newId) }
siteSettingsDao().insert(settingsWithSiteId)
lastResultWithSiteId?.let { validationResultsDao().insert(it) }
retryPolicyWithSiteId?.let { retryPolicyDao().insert(it) }
headerDao().insert(headersWithSiteId)
return site.copy(
id = newId,
settings = settingsWithSiteId,
lastResult = lastResultWithSiteId,
retryPolicy = retryPolicyWithSiteId,
headers = headersWithSiteId
settings = settingsWithSiteId
)
}
@ -165,13 +151,6 @@ fun AppDatabase.updateSite(site: Site) {
retryPolicyDao().insert(retryPolicy)
}
}
// Wipe existing headers
headerDao().delete(headerDao().forSite(site.id))
// Then add ones that still exist
site.headers.forEach { header ->
headerDao().insert(header.copy(id = 0, siteId = site.id))
}
}
/**
@ -183,9 +162,5 @@ fun AppDatabase.deleteSite(site: Site) {
site.settings?.let { siteSettingsDao().delete(it) }
site.lastResult?.let { validationResultsDao().delete(it) }
site.retryPolicy?.let { retryPolicyDao().delete(it) }
if (site.headers.any { it.id == 0L }) {
throw IllegalStateException("Cannot delete header with ID = 0.")
}
headerDao().delete(site.headers)
siteDao().delete(site)
}

View file

@ -27,45 +27,7 @@ class Database1to2Migration : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"CREATE TABLE IF NOT EXISTS `retry_policies` (siteId INTEGER PRIMARY KEY NOT NULL, count INTEGER NOT NULL, minutes INTEGER NOT NULL, lastTryTimestamp INTEGER NOT NULL, triesLeft INTEGER NOT NULL)"
"CREATE TABLE `retry_policies` (siteId INTEGER PRIMARY KEY NOT NULL, count INTEGER NOT NULL, minutes INTEGER NOT NULL)"
)
}
}
/**
* Migrates the database from version 2 to 3.
*
* @author Aidan Follestad (@afollestad)
*/
class Database2to3Migration : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `sites` ADD COLUMN tags TEXT NOT NULL DEFAULT ''")
}
}
/**
* Migrates the database from version 3 to 4.
*
* @author Aidan Follestad (@afollestad)
*/
class Database3to4Migration : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"CREATE TABLE IF NOT EXISTS `headers` (id INTEGER PRIMARY KEY NOT NULL, siteId INTEGER NOT NULL, `key` TEXT NOT NULL, value TEXT NOT NULL)"
)
}
}
/**
* 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")
}
}

View file

@ -1,47 +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.data
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy.FAIL
import androidx.room.Query
import androidx.room.Update
import com.afollestad.nocknock.data.model.Header
/** @author Aidan Follestad (@afollestad) */
@Dao
interface HeaderDao {
@Query("SELECT * FROM headers ORDER BY siteId ASC")
fun all(): List<Header>
@Query("SELECT * FROM headers WHERE siteId = :siteId")
fun forSite(siteId: Long): List<Header>
@Insert(onConflict = FAIL)
fun insert(headers: Header): Long
@Insert(onConflict = FAIL)
fun insert(headers: List<Header>): List<Long>
@Update(onConflict = FAIL)
fun update(header: Header): Int
@Delete
fun delete(headers: List<Header>): Int
}

View file

@ -15,7 +15,7 @@
*/
@file:Suppress("unused")
package com.afollestad.nocknock.data.model
package com.afollestad.nocknock.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@ -57,14 +57,6 @@ data class RetryPolicy(
return -1
}
val timesPerMinute = count.toFloat() / minutes.toFloat()
return MINUTE / timesPerMinute.toSafeInt()
}
private fun Float.toSafeInt(): Int {
val intValue = toInt()
if (intValue == 0) {
return 1
}
return intValue
return MINUTE / timesPerMinute.toInt()
}
}

View file

@ -21,7 +21,6 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy.FAIL
import androidx.room.Query
import androidx.room.Update
import com.afollestad.nocknock.data.model.RetryPolicy
/** @author Aidan Follestad (@afollestad) */
@Dao

View file

@ -1,42 +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.
*/
@file:Suppress("unused")
package com.afollestad.nocknock.data.model
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
/**
* Represents an HTTP header that is sent with a site's validation attempts.
*
* @author Aidan Follestad (@afollestad)
*/
@Entity(tableName = "headers")
data class Header(
/** The header's unique datrabase ID. */
@PrimaryKey(autoGenerate = true) var id: Long = 0,
/** The [Site] this header belong to. */
var siteId: Long = 0,
/** The header key/name. */
var key: String = "",
/** The header value. */
var value: String = ""
) : Serializable {
constructor() : this(0, 0, "", "")
}

View file

@ -18,6 +18,7 @@ package com.afollestad.nocknock.data.model
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.afollestad.nocknock.data.RetryPolicy
import com.afollestad.nocknock.data.model.Status.WAITING
import com.afollestad.nocknock.utilities.ext.timeString
import com.afollestad.nocknock.utilities.providers.CanNotifyModel
@ -31,21 +32,17 @@ data class Site(
@PrimaryKey(autoGenerate = true) var id: Long = 0,
/** The site's user-given name. */
var name: String,
/** The URL at which validation attempts are made to. */
/** The URl at which validation attempts are made to. */
var url: String,
/** Comma separated tags for this site. */
var tags: String,
/** Settings for the site. */
@Ignore var settings: SiteSettings?,
/** The last validation attempt result for the site, if any. */
@Ignore var lastResult: ValidationResult?,
/** The site's retry policy, if any. */
@Ignore var retryPolicy: RetryPolicy?,
/** Request headers sent with this site's validation attempts. */
@Ignore var headers: List<Header>
@Ignore var retryPolicy: RetryPolicy?
) : CanNotifyModel {
constructor() : this(0, "", "", "", null, null, null, emptyList())
constructor() : this(0, "", "", null, null, null)
override fun notifyId(): Int = id.toInt()
@ -53,8 +50,6 @@ data class Site(
override fun notifyTag(): String = url
override fun notifyDescription() = lastResult?.reason
fun intervalText(): String {
requireNotNull(settings) { "Settings not queried." }
val lastCheck = lastResult?.timestampMs ?: -1

View file

@ -40,10 +40,8 @@ data class SiteSettings(
/** Whether or not the [Site] is enabled for automatic periodic checks. */
var disabled: Boolean,
/** The network response timeout for validation attempts. */
var networkTimeout: Int,
/** The Uri to a self signed certificate. */
var certificate: String?
var networkTimeout: Int
) : Serializable {
constructor() : this(0, 0, STATUS_CODE, null, false, 0, null)
constructor() : this(0, 0, STATUS_CODE, null, false, 0)
}

View file

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

View file

@ -15,18 +15,14 @@
*/
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.ValidationExecutor
import com.afollestad.nocknock.engine.validation.RealValidationManager
import com.afollestad.nocknock.engine.validation.ValidationManager
import org.koin.dsl.module.module
/** @author Aidan Follestad (@afollestad) */
val engineModule = module {
single {
RealValidationExecutor(get(), get(), get(), get(), get(), get(), get())
} bind ValidationExecutor::class
factory { RealSslManager(get()) } bind SslManager::class
RealValidationManager(get(), get(), get(), get(), get(), get())
} bind ValidationManager::class
}

View file

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

View file

@ -32,7 +32,7 @@ import timber.log.Timber.d as log
/** @author Aidan Follestad (@afollestad) */
class BootReceiver : BroadcastReceiver(), KoinComponent {
private val validationManager by inject<ValidationExecutor>()
private val validationManager by inject<ValidationManager>()
private val mainDispatcher by inject<CoroutineDispatcher>(name = MAIN_DISPATCHER)
private val ioDispatcher by inject<CoroutineDispatcher>(name = IO_DISPATCHER)
@ -48,7 +48,7 @@ class BootReceiver : BroadcastReceiver(), KoinComponent {
val pendingResult = goAsync()
GlobalScope.launch(mainDispatcher) {
withContext(ioDispatcher) { validationManager.ensureScheduledValidations() }
withContext(ioDispatcher) { validationManager.ensureScheduledChecks() }
pendingResult.resultCode = 0
pendingResult.finish()
}

View file

@ -19,8 +19,8 @@ import android.app.job.JobParameters
import android.app.job.JobService
import android.content.Intent
import com.afollestad.nocknock.data.AppDatabase
import com.afollestad.nocknock.data.RetryPolicy
import com.afollestad.nocknock.data.getSite
import com.afollestad.nocknock.data.model.RetryPolicy
import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.data.model.Status
import com.afollestad.nocknock.data.model.Status.CHECKING
@ -60,7 +60,7 @@ class ValidationJob : JobService() {
}
private val database by inject<AppDatabase>()
private val validationManager by inject<ValidationExecutor>()
private val validationManager by inject<ValidationManager>()
private val notificationManager by inject<NockNotificationManager>()
override fun onStartJob(params: JobParameters): Boolean {
@ -80,14 +80,10 @@ class ValidationJob : JobService() {
sendBroadcast(Intent(ACTION_JOB_RUNNING).apply { putExtra(KEY_SITE_ID, site.id) })
log("Checking ${site.name} (${site.url})...")
val lastResult = site.lastResult
if (lastResult != null) {
log("Result of previous attempt: ${lastResult.status}")
}
val jobResult = async(IO) {
updateStatus(site, CHECKING)
val checkResult = validationManager.performValidation(site)
val checkResult = validationManager.performCheck(site)
val resultModel = checkResult.model
val resultResponse = checkResult.response
val result = resultModel.lastResult!!
@ -143,9 +139,6 @@ class ValidationJob : JobService() {
if (jobResult.lastResult!!.status == OK) {
notificationManager.cancelStatusNotification(jobResult)
if (lastResult != null && lastResult.status == ERROR) {
notificationManager.postValidationSuccessNotification(jobResult)
}
} else {
val retryPolicy = site.retryPolicy
if (retryPolicy != null) {
@ -160,7 +153,7 @@ class ValidationJob : JobService() {
updateTriesLeft(retryPolicy, retryPolicy.triesLeft)
val interval = retryPolicy.interval()
validationManager.scheduleValidation(
validationManager.scheduleCheck(
site = jobResult,
fromFinishingJob = true,
overrideDelay = interval
@ -174,10 +167,10 @@ class ValidationJob : JobService() {
}
}
notificationManager.postValidationErrorNotification(jobResult)
notificationManager.postStatusNotification(jobResult)
}
validationManager.scheduleValidation(
validationManager.scheduleCheck(
site = jobResult,
fromFinishingJob = true
)
@ -232,7 +225,6 @@ class ValidationJob : JobService() {
triesLeft: Int
) {
retryPolicy.triesLeft = triesLeft
retryPolicy.lastTryTimestamp = currentTimeMillis()
withContext(IO) {
database.retryPolicyDao()
.update(retryPolicy)

View file

@ -17,26 +17,21 @@ package com.afollestad.nocknock.engine.validation
import android.app.job.JobScheduler
import android.app.job.JobScheduler.RESULT_SUCCESS
import android.net.Uri
import com.afollestad.nocknock.data.AppDatabase
import com.afollestad.nocknock.data.allSites
import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.data.model.Status.ERROR
import com.afollestad.nocknock.data.model.Status.OK
import com.afollestad.nocknock.engine.R
import com.afollestad.nocknock.engine.ssl.SslManager
import com.afollestad.nocknock.engine.validation.ValidationJob.Companion.KEY_SITE_ID
import com.afollestad.nocknock.utilities.ext.isNotNullOrEmpty
import com.afollestad.nocknock.utilities.providers.BundleProvider
import com.afollestad.nocknock.utilities.providers.JobInfoProvider
import com.afollestad.nocknock.utilities.providers.StringProvider
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jetbrains.annotations.TestOnly
import java.net.SocketTimeoutException
import java.util.concurrent.TimeUnit.MILLISECONDS
import kotlin.math.max
import timber.log.Timber.d as log
/** @author Aidan Follestad (@afollestad) */
@ -47,14 +42,12 @@ data class CheckResult(
typealias ClientTimeoutChanger = (client: OkHttpClient, timeout: Int) -> OkHttpClient
typealias UriConverter = (String) -> Uri
/** @author Aidan Follestad (@afollestad) */
interface ValidationExecutor {
interface ValidationManager {
suspend fun ensureScheduledValidations()
suspend fun ensureScheduledChecks()
fun scheduleValidation(
fun scheduleCheck(
site: Site,
rightNow: Boolean = false,
cancelPrevious: Boolean = rightNow,
@ -62,20 +55,19 @@ interface ValidationExecutor {
overrideDelay: Long = -1
)
fun cancelScheduledValidation(site: Site)
fun cancelCheck(site: Site)
suspend fun performValidation(site: Site): CheckResult
suspend fun performCheck(site: Site): CheckResult
}
class RealValidationExecutor(
class RealValidationManager(
private val jobScheduler: JobScheduler,
private val okHttpClient: OkHttpClient,
private val stringProvider: StringProvider,
private val bundleProvider: BundleProvider,
private val jobInfoProvider: JobInfoProvider,
private val database: AppDatabase,
private val sslManager: SslManager
) : ValidationExecutor {
private val database: AppDatabase
) : ValidationManager {
private var clientTimeoutChanger: ClientTimeoutChanger = { client, timeout ->
client.newBuilder()
@ -83,37 +75,37 @@ class RealValidationExecutor(
.build()
}
override suspend fun ensureScheduledValidations() {
override suspend fun ensureScheduledChecks() {
val sites = database.allSites()
if (sites.isEmpty()) {
return
}
log("Ensuring enabled sites have scheduled validations.")
log("Ensuring enabled sites have scheduled checks.")
sites.filter { it.settings?.disabled != true }
.forEach { site ->
val existingJob = jobForSite(site)
if (existingJob == null) {
log("Site ${site.id} does NOT have a scheduled job, running one now.")
scheduleValidation(site = site, rightNow = true)
scheduleCheck(site = site, rightNow = true)
} else {
log("Site ${site.id} already has a scheduled job. Nothing to do.")
}
}
}
override fun scheduleValidation(
override fun scheduleCheck(
site: Site,
rightNow: Boolean,
cancelPrevious: Boolean,
fromFinishingJob: Boolean,
overrideDelay: Long
) {
check(site.id != 0L) { "Cannot schedule validations for jobs with no ID." }
check(site.id != 0L) { "Cannot schedule checks for jobs with no ID." }
val siteSettings = site.settings
requireNotNull(siteSettings) { "Site settings must be populated." }
if (cancelPrevious) {
cancelScheduledValidation(site)
cancelCheck(site)
} else if (!fromFinishingJob) {
val existingJob = jobForSite(site)
check(existingJob == null) {
@ -121,7 +113,7 @@ class RealValidationExecutor(
}
}
log("Requesting a validation job for site to be scheduled: $site")
log("Requesting a check job for site to be scheduled: $site")
val extras = bundleProvider.createPersistable {
putLong(KEY_SITE_ID, site.id)
}
@ -139,59 +131,43 @@ class RealValidationExecutor(
val dispatchResult = jobScheduler.schedule(jobInfo)
if (dispatchResult != RESULT_SUCCESS) {
log("Failed to schedule a validation job for site: ${site.id}")
log("Failed to schedule a check job for site: ${site.id}")
} else {
log("Validation job successfully scheduled for site: ${site.id}")
log("Check job successfully scheduled for site: ${site.id}")
}
}
override fun cancelScheduledValidation(site: Site) {
check(site.id != 0L) { "Cannot cancel scheduled validations for jobs with no ID." }
log("Cancelling scheduled validations for site: ${site.id}")
override fun cancelCheck(site: Site) {
check(site.id != 0L) { "Cannot cancel scheduled checks for jobs with no ID." }
log("Cancelling scheduled checks for site: ${site.id}")
jobScheduler.cancel(site.id.toInt())
}
override suspend fun performValidation(site: Site): CheckResult {
check(site.id != 0L) { "Cannot schedule validations for jobs with no ID." }
override suspend fun performCheck(site: Site): CheckResult {
check(site.id != 0L) { "Cannot schedule checks for jobs with no ID." }
val siteSettings = site.settings
requireNotNull(siteSettings) { "Site settings must be populated." }
log("performValidation(${site.id}) - GET ${site.url}")
check(siteSettings.networkTimeout > 0) { "Network timeout not set for site ${site.id}" }
log("performCheck(${site.id}) - GET ${site.url}")
val request = Request.Builder()
.apply {
url(site.url)
get()
site.headers
.filter { header -> header.key.isNotNullOrEmpty() }
.forEach { header ->
addHeader(header.key, header.value)
}
}
.url(site.url)
.get()
.build()
return try {
val timeout = max(siteSettings.networkTimeout, 1)
val clientWithTimeout = clientTimeoutChanger(okHttpClient, timeout)
val client = if (siteSettings.certificate.isNotNullOrEmpty()) {
sslManager.clientForCertificate(
certUri = siteSettings.certificate!!,
siteUri = site.url,
client = clientWithTimeout
)
} else {
clientWithTimeout
}
val client = clientTimeoutChanger(okHttpClient, siteSettings.networkTimeout)
val response = client.newCall(request)
.execute()
if (response.isSuccessful) {
log("performValidation(${site.id}) = Successful")
if (response.isSuccessful || response.code() == 401) {
log("performCheck(${site.id}) = Successful")
CheckResult(
model = site.withStatus(status = OK, reason = null),
response = response
)
} else {
log("performValidation(${site.id}) = Failure, HTTP code ${response.code()}")
log("performCheck(${site.id}) = Failure, HTTP code ${response.code()}")
CheckResult(
model = site.withStatus(
status = ERROR,
@ -201,7 +177,7 @@ class RealValidationExecutor(
)
}
} catch (timeoutEx: SocketTimeoutException) {
log("performValidation(${site.id}) = Socket Timeout")
log("performCheck(${site.id}) = Socket Timeout")
CheckResult(
model = site.withStatus(
status = ERROR,
@ -209,8 +185,7 @@ class RealValidationExecutor(
)
)
} catch (ex: Exception) {
ex.printStackTrace()
log("performValidation(${site.id}) = Error: ${ex.message}")
log("performCheck(${site.id}) = Error: ${ex.message}")
CheckResult(model = site.withStatus(status = ERROR, reason = ex.message))
}
}
@ -219,7 +194,7 @@ class RealValidationExecutor(
jobScheduler.allPendingJobs
.firstOrNull { job -> job.id == site.id.toInt() }
@TestOnly fun setClientTimeoutChanger(changer: ClientTimeoutChanger) {
this.clientTimeoutChanger = changer
}
// @TestOnly fun setClientTimeoutChanger(changer: ClientTimeoutChanger) {
// this.clientTimeoutChanger = changer
// }
}

Some files were not shown because too many files have changed in this diff Show more