Compare commits
No commits in common. "master" and "0.8.2b" have entirely different histories.
3
.editorconfig
Normal file
|
@ -0,0 +1,3 @@
|
|||
[*.kt]
|
||||
indent_size = 2
|
||||
continuation_indent_size=4
|
28
.github/ISSUE_TEMPLATE.md
vendored
Normal 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
|
28
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
|
@ -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.
|
15
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
|
@ -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.
|
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,19 @@
|
|||
language: android
|
||||
jdk: oraclejdk8
|
||||
before_script:
|
||||
- echo no | android create avd --force -n test -t android-22 --abi armeabi-v7a
|
||||
- emulator -avd test -no-audio -no-window &
|
||||
- android-wait-for-emulator
|
||||
- adb shell input keyevent 82 &
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-28.0.3
|
||||
- android-28
|
||||
- extra-android-support
|
||||
- extra-android-m2repository
|
||||
- extra-google-m2repository
|
||||
|
||||
licenses:
|
||||
- '.+'
|
|
@ -1,8 +1,9 @@
|
|||
## Nock Nock
|
||||
|
||||
[](https://travis-ci.org/afollestad/nock-nock)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
|
||||

|
||||

|
||||
|
||||
Nock Nock is a simple app which allows you to monitor your websites for maximum uptime.
|
||||
|
||||
|
|
|
@ -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'
|
|
@ -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"/>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.ui
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
enum class NightMode {
|
||||
/** Night mode is on at the system level. */
|
||||
ENABLED,
|
||||
/** Night mode is off at the system level. */
|
||||
DISABLED,
|
||||
/** We don't know about night mode, fallback to custom impl. */
|
||||
UNKNOWN
|
||||
}
|
|
@ -16,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() ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
/>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 7 KiB |
Before Width: | Height: | Size: 9 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 15 KiB |
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#758F9A</color>
|
||||
</resources>
|
|
@ -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>
|
||||
<a href=\'https://twitter.com/afollestad\'>Twitter</a>
|
||||
<a href=\'https://github.com/afollestad\'>GitHub</a>
|
||||
<a href=\'https://www.linkedin.com/in/afollestad\'>LinkedIn</a>
|
||||
<br/><br/><i>Nock Nock is open source! Check out the <a href=\'https://github.com/afollestad/nock-nock\'>GitHub page</a>!</i>
|
||||
<br/>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 & 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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!!)
|
||||
|
|
|
@ -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")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 528 KiB |
BIN
art/showcasemain3.png
Normal file
After Width: | Height: | Size: 678 KiB |
|
@ -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" }
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.utilities.ext
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
fun String.toUri() = Uri.parse(this)!!
|
||||
|
||||
fun String?.isNotNullOrEmpty(): Boolean {
|
||||
if (this == null || this == "null") {
|
||||
return false
|
||||
}
|
||||
return !isNullOrEmpty()
|
||||
}
|
|
@ -21,12 +21,7 @@ import android.widget.EditText
|
|||
import androidx.annotation.IntRange
|
||||
import 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)
|
||||
|
|
|
@ -30,8 +30,6 @@ interface CanNotifyModel : Serializable {
|
|||
fun notifyName(): String
|
||||
|
||||
fun notifyTag(): String
|
||||
|
||||
fun notifyDescription(): String?
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,10 +14,6 @@ android {
|
|||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/atomicfu.kotlin_module'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<application>
|
||||
<uses-library
|
||||
android:name="androidx.test.runner"
|
||||
android:name="android.test.runner"
|
||||
android:required="false"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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, "", "")
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
// }
|
||||
}
|