Compare commits
69 commits
Author | SHA1 | Date | |
---|---|---|---|
|
23ba4a69cd |
||
|
dd9aec1dff |
||
|
406af590aa | ||
|
550f8c59be | ||
|
10d7fe33f9 | ||
|
35eda8f057 | ||
|
a0fd44ae7a | ||
|
351f718df8 | ||
|
e2f7db22d1 | ||
|
82c1a17c68 | ||
|
a6670e2bea | ||
|
5fc1569099 | ||
|
0770db5df5 | ||
|
97a0eda92c | ||
|
1ccb89bfc3 | ||
|
9ea9c78099 | ||
|
997c797598 |
||
|
b26543d244 |
||
|
8c3654c4ac | ||
|
df2652860e | ||
|
4da8cb5f11 | ||
|
334e9e823c | ||
|
6d382b93a5 | ||
|
ef18464728 |
||
|
872e99d80d | ||
|
7f507792a8 |
||
|
68b6944542 | ||
|
e39093b526 | ||
|
9514a5ec83 | ||
|
3e5b1d4d8e | ||
|
de59bf9ec1 | ||
|
0fbd27b54b | ||
|
33388bd5c2 | ||
|
75297c7ff5 | ||
|
c6fca52fe4 | ||
|
b3f8a43f71 | ||
|
7dc4ee7fb1 | ||
|
859dcb53ca | ||
|
f86ccbbe0c | ||
|
571e7ebff3 | ||
|
77f939b095 | ||
|
8f16ff2d33 | ||
|
4f5fec758e | ||
|
b369f9dfd3 | ||
|
38c8c92c1c | ||
|
6ae85ea061 | ||
|
34329f3a9f | ||
|
6bb131fb23 | ||
|
8535a6fe8b | ||
|
cd1651672f | ||
|
26d6d9abf8 | ||
|
909e5420ad | ||
|
55ea6674e6 | ||
|
2221c45789 | ||
|
deae0f0dc2 | ||
|
f207ed5f78 | ||
|
cbac2796aa | ||
|
e3820fd7d3 | ||
|
8dc2112e2d | ||
|
74f7aa8aa2 | ||
|
646bc25232 | ||
|
26ab76b363 | ||
|
56030af0f0 | ||
|
7f8db7b7d5 | ||
|
d293a83240 | ||
|
002149cd3f | ||
|
2756fc9fc7 | ||
|
67aa54ac22 | ||
|
2fe6f171ba |
|
@ -1,3 +0,0 @@
|
||||||
[*.kt]
|
|
||||||
indent_size = 2
|
|
||||||
continuation_indent_size=4
|
|
28
.github/ISSUE_TEMPLATE.md
vendored
|
@ -1,28 +0,0 @@
|
||||||
(`[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
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
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
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
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,9 +1,8 @@
|
||||||
|
|
||||||
### Guidelines
|
### Guidelines
|
||||||
|
|
||||||
1. You must run the `spotlessApply` task before commiting, either through Android Studio or with `./gradlew spotlessApply`.
|
1. You must run the `spotlessApply` task before committing, 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.
|
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.
|
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.
|
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,4 +180,6 @@ gradle-app.setting
|
||||||
.gradletasknamecache
|
.gradletasknamecache
|
||||||
|
|
||||||
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||||
# gradle/wrapper/gradle-wrapper.properties
|
# gradle/wrapper/gradle-wrapper.properties
|
||||||
|
|
||||||
|
app/google-services.json
|
37
.idea/misc.xml
generated
|
@ -5,7 +5,42 @@
|
||||||
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
|
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
|
||||||
</configurations>
|
</configurations>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
<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">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|
19
.travis.yml
|
@ -1,19 +0,0 @@
|
||||||
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,9 +1,8 @@
|
||||||
## Nock Nock
|
## Nock Nock
|
||||||
|
|
||||||
[](https://travis-ci.org/afollestad/nock-nock)
|
|
||||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Nock Nock is a simple app which allows you to monitor your websites for maximum uptime.
|
Nock Nock is a simple app which allows you to monitor your websites for maximum uptime.
|
||||||
|
|
||||||
|
|
|
@ -4,18 +4,6 @@ apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
|
||||||
apply plugin: 'io.fabric'
|
|
||||||
|
|
||||||
def getFabricApiKey() {
|
|
||||||
def propsFile = project.rootProject.file('local.properties')
|
|
||||||
if (!propsFile.exists()) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
Properties properties = new Properties()
|
|
||||||
properties.load(propsFile.newDataInputStream())
|
|
||||||
return properties.getProperty("fabric.apikey") ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion versions.compileSdk
|
compileSdkVersion versions.compileSdk
|
||||||
buildToolsVersion versions.buildTools
|
buildToolsVersion versions.buildTools
|
||||||
|
@ -26,16 +14,15 @@ android {
|
||||||
targetSdkVersion versions.compileSdk
|
targetSdkVersion versions.compileSdk
|
||||||
versionCode versions.publishVersionCode
|
versionCode versions.publishVersionCode
|
||||||
versionName versions.publishVersion
|
versionName versions.publishVersion
|
||||||
manifestPlaceholders = [fabricKey:getFabricApiKey()]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
compileOptions {
|
||||||
debug {
|
sourceCompatibility 1.8
|
||||||
buildConfigField "String", "FABRIC_API_KEY", "\"\""
|
targetCompatibility 1.8
|
||||||
}
|
}
|
||||||
release {
|
|
||||||
buildConfigField "String", "FABRIC_API_KEY", "\"${getFabricApiKey()}\""
|
packagingOptions {
|
||||||
}
|
exclude 'META-INF/atomicfu.kotlin_module'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +38,7 @@ dependencies {
|
||||||
implementation 'androidx.recyclerview:recyclerview:' + versions.androidxRecyclerView
|
implementation 'androidx.recyclerview:recyclerview:' + versions.androidxRecyclerView
|
||||||
implementation 'com.google.android.material:material:' + versions.googleMaterial
|
implementation 'com.google.android.material:material:' + versions.googleMaterial
|
||||||
implementation 'androidx.browser:browser:' + versions.androidxBrowser
|
implementation 'androidx.browser:browser:' + versions.androidxBrowser
|
||||||
|
implementation 'com.google.firebase:firebase-core:' + versions.firebaseCore
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
kapt 'androidx.lifecycle:lifecycle-compiler:' + versions.lifecycle
|
kapt 'androidx.lifecycle:lifecycle-compiler:' + versions.lifecycle
|
||||||
|
@ -84,4 +72,8 @@ dependencies {
|
||||||
androidTestImplementation 'androidx.test:rules:' + versions.androidxTestRunner
|
androidTestImplementation 'androidx.test:rules:' + versions.androidxTestRunner
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: '../spotless.gradle'
|
apply from: '../spotless.gradle'
|
||||||
|
apply from: '../mock/mock.gradle'
|
||||||
|
|
||||||
|
apply plugin: "io.fabric"
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
|
@ -50,9 +50,6 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="io.fabric.ApiKey"
|
|
||||||
android:value="${fabricKey}"/>
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="preloaded_fonts"
|
android:name="preloaded_fonts"
|
||||||
android:resource="@array/preloaded_fonts"/>
|
android:resource="@array/preloaded_fonts"/>
|
||||||
|
|
|
@ -20,12 +20,12 @@ import android.app.Application
|
||||||
import android.app.Application.ActivityLifecycleCallbacks
|
import android.app.Application.ActivityLifecycleCallbacks
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||||
import androidx.core.text.HtmlCompat.fromHtml
|
import androidx.core.text.HtmlCompat.fromHtml
|
||||||
import com.afollestad.materialdialogs.utils.MDUtil.resolveColor
|
import com.afollestad.materialdialogs.utils.MDUtil.resolveColor
|
||||||
|
import com.afollestad.nocknock.utilities.ext.toUri
|
||||||
import com.afollestad.nocknock.utilities.ui.toast
|
import com.afollestad.nocknock.utilities.ui.toast
|
||||||
|
|
||||||
typealias ActivityLifeChange = (activity: Activity, resumed: Boolean) -> Unit
|
typealias ActivityLifeChange = (activity: Activity, resumed: Boolean) -> Unit
|
||||||
|
@ -57,8 +57,6 @@ fun Application.onActivityLifeChange(cb: ActivityLifeChange) {
|
||||||
|
|
||||||
fun String.toHtml() = fromHtml(this, FROM_HTML_MODE_LEGACY)
|
fun String.toHtml() = fromHtml(this, FROM_HTML_MODE_LEGACY)
|
||||||
|
|
||||||
fun String.toUri() = Uri.parse(this)!!
|
|
||||||
|
|
||||||
fun Activity.viewUrl(url: String) {
|
fun Activity.viewUrl(url: String) {
|
||||||
val customTabsIntent = CustomTabsIntent.Builder()
|
val customTabsIntent = CustomTabsIntent.Builder()
|
||||||
.apply {
|
.apply {
|
||||||
|
|
|
@ -47,10 +47,8 @@ class NockNockApp : Application() {
|
||||||
Timber.plant(DebugTree())
|
Timber.plant(DebugTree())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BuildConfig.FABRIC_API_KEY.isNotEmpty()) {
|
Timber.plant(FabricTree())
|
||||||
Timber.plant(FabricTree())
|
Fabric.with(this, Crashlytics())
|
||||||
Fabric.with(this, Crashlytics())
|
|
||||||
}
|
|
||||||
|
|
||||||
val modules = listOf(
|
val modules = listOf(
|
||||||
prefModule,
|
prefModule,
|
||||||
|
|
115
app/src/main/java/com/afollestad/nocknock/adapter/TagAdapter.kt
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
/**
|
||||||
|
* 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,6 +20,7 @@ import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.afollestad.nocknock.BuildConfig
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
|
@ -34,8 +35,9 @@ class AboutDialog : DialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
return MaterialDialog(activity!!)
|
val context = activity ?: throw IllegalStateException("Oh no!")
|
||||||
.title(R.string.about)
|
return MaterialDialog(context)
|
||||||
|
.title(text = getString(R.string.app_name_x, BuildConfig.VERSION_NAME))
|
||||||
.positiveButton(R.string.dismiss)
|
.positiveButton(R.string.dismiss)
|
||||||
.message(R.string.about_body, html = true, lineHeightMultiplier = 1.4f)
|
.message(R.string.about_body, html = true, lineHeightMultiplier = 1.4f)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,9 @@ import android.content.Context.NOTIFICATION_SERVICE
|
||||||
import androidx.room.Room.databaseBuilder
|
import androidx.room.Room.databaseBuilder
|
||||||
import com.afollestad.nocknock.data.AppDatabase
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.data.Database1to2Migration
|
import com.afollestad.nocknock.data.Database1to2Migration
|
||||||
|
import com.afollestad.nocknock.data.Database2to3Migration
|
||||||
|
import com.afollestad.nocknock.data.Database3to4Migration
|
||||||
|
import com.afollestad.nocknock.data.Database4to5Migration
|
||||||
import com.afollestad.nocknock.notifications.Qualifiers.MAIN_ACTIVITY_CLASS
|
import com.afollestad.nocknock.notifications.Qualifiers.MAIN_ACTIVITY_CLASS
|
||||||
import com.afollestad.nocknock.ui.main.MainActivity
|
import com.afollestad.nocknock.ui.main.MainActivity
|
||||||
import com.afollestad.nocknock.utilities.ext.systemService
|
import com.afollestad.nocknock.utilities.ext.systemService
|
||||||
|
@ -38,7 +41,12 @@ val mainModule = module {
|
||||||
|
|
||||||
single {
|
single {
|
||||||
databaseBuilder(get(), AppDatabase::class.java, "NockNock.db")
|
databaseBuilder(get(), AppDatabase::class.java, "NockNock.db")
|
||||||
.addMigrations(Database1to2Migration())
|
.addMigrations(
|
||||||
|
Database1to2Migration(),
|
||||||
|
Database2to3Migration(),
|
||||||
|
Database3to4Migration(),
|
||||||
|
Database4to5Migration()
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,15 @@
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.ui
|
package com.afollestad.nocknock.ui
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.koin.PREF_DARK_MODE
|
import com.afollestad.nocknock.koin.PREF_DARK_MODE
|
||||||
|
import com.afollestad.nocknock.ui.NightMode.DISABLED
|
||||||
|
import com.afollestad.nocknock.ui.NightMode.ENABLED
|
||||||
|
import com.afollestad.nocknock.ui.NightMode.UNKNOWN
|
||||||
import com.afollestad.nocknock.utilities.rx.attachLifecycle
|
import com.afollestad.nocknock.utilities.rx.attachLifecycle
|
||||||
import com.afollestad.rxkprefs.Pref
|
import com.afollestad.rxkprefs.Pref
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
@ -35,16 +40,35 @@ abstract class DarkModeSwitchActivity : AppCompatActivity() {
|
||||||
setTheme(themeRes())
|
setTheme(themeRes())
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
darkModePref.observe()
|
if (getCurrentNightMode() == UNKNOWN) {
|
||||||
.filter { it != isDarkModeEnabled }
|
darkModePref.observe()
|
||||||
.subscribe {
|
.filter { it != isDarkModeEnabled }
|
||||||
log("Theme changed, recreating Activity.")
|
.subscribe {
|
||||||
recreate()
|
log("Theme changed, recreating Activity.")
|
||||||
}
|
recreate()
|
||||||
.attachLifecycle(this)
|
}
|
||||||
|
.attachLifecycle(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun isDarkMode() = darkModePref.get()
|
protected fun getCurrentNightMode(): NightMode {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||||
|
return UNKNOWN
|
||||||
|
}
|
||||||
|
return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
|
||||||
|
Configuration.UI_MODE_NIGHT_YES -> return ENABLED
|
||||||
|
Configuration.UI_MODE_NIGHT_NO -> return DISABLED
|
||||||
|
else -> UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun isDarkMode(): Boolean {
|
||||||
|
return when (getCurrentNightMode()) {
|
||||||
|
ENABLED -> true
|
||||||
|
DISABLED -> false
|
||||||
|
else -> darkModePref.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected fun toggleDarkMode() = setDarkMode(!isDarkMode())
|
protected fun toggleDarkMode() = setDarkMode(!isDarkMode())
|
||||||
|
|
||||||
|
|
26
app/src/main/java/com/afollestad/nocknock/ui/NightMode.kt
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* Designed and developed by Aidan Follestad (@afollestad)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.afollestad.nocknock.ui
|
||||||
|
|
||||||
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
|
enum class NightMode {
|
||||||
|
/** Night mode is on at the system level. */
|
||||||
|
ENABLED,
|
||||||
|
/** Night mode is off at the system level. */
|
||||||
|
DISABLED,
|
||||||
|
/** We don't know about night mode, fallback to custom impl. */
|
||||||
|
UNKNOWN
|
||||||
|
}
|
|
@ -16,18 +16,32 @@
|
||||||
package com.afollestad.nocknock.ui.addsite
|
package com.afollestad.nocknock.ui.addsite
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.Intent.ACTION_OPEN_DOCUMENT
|
||||||
|
import android.content.Intent.CATEGORY_OPENABLE
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode
|
import com.afollestad.nocknock.data.model.ValidationMode
|
||||||
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
||||||
|
import com.afollestad.nocknock.ui.viewsite.KEY_SITE
|
||||||
|
import com.afollestad.nocknock.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.attachLiveData
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
|
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
|
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||||
|
import com.afollestad.vvalidator.form
|
||||||
|
import com.afollestad.vvalidator.form.Form
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout
|
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.doneBtn
|
import kotlinx.android.synthetic.main.activity_addsite.headersLayout
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.inputName
|
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.inputUrl
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.loadingProgress
|
import kotlinx.android.synthetic.main.activity_addsite.loadingProgress
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.responseTimeoutInput
|
import kotlinx.android.synthetic.main.activity_addsite.responseTimeoutInput
|
||||||
|
@ -35,44 +49,54 @@ import kotlinx.android.synthetic.main.activity_addsite.responseValidationMode
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchTerm
|
import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchTerm
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.retryPolicyLayout
|
import kotlinx.android.synthetic.main.activity_addsite.retryPolicyLayout
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout
|
import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout
|
||||||
|
import kotlinx.android.synthetic.main.activity_addsite.scrollView
|
||||||
|
import kotlinx.android.synthetic.main.activity_addsite.sslCertificateBrowse
|
||||||
|
import kotlinx.android.synthetic.main.activity_addsite.sslCertificateInput
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning
|
import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
|
import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
|
||||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
import kotlinx.android.synthetic.main.include_app_bar.app_toolbar as appToolbar
|
||||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
|
import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class AddSiteActivity : DarkModeSwitchActivity() {
|
class AddSiteActivity : DarkModeSwitchActivity() {
|
||||||
|
companion object {
|
||||||
|
private const val SELECT_CERT_FILE_RQ = 23
|
||||||
|
}
|
||||||
|
|
||||||
private val viewModel by viewModel<AddSiteViewModel>()
|
private val viewModel by viewModel<AddSiteViewModel>()
|
||||||
|
private lateinit var validationForm: Form
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_addsite)
|
setContentView(R.layout.activity_addsite)
|
||||||
setupUi()
|
setupUi()
|
||||||
|
setupValidation()
|
||||||
|
|
||||||
lifecycle.addObserver(viewModel)
|
lifecycle.addObserver(viewModel)
|
||||||
|
|
||||||
|
// Populate view model with initial data
|
||||||
|
val model = intent.getSerializableExtra(KEY_SITE) as? Site
|
||||||
|
model?.let { viewModel.prePopulateFromModel(model) }
|
||||||
|
|
||||||
// Loading
|
// Loading
|
||||||
loadingProgress.observe(this, viewModel.onIsLoading())
|
loadingProgress.observe(this, viewModel.onIsLoading())
|
||||||
|
|
||||||
// Name
|
// Name
|
||||||
inputName.attachLiveData(this, viewModel.name)
|
inputName.attachLiveData(this, viewModel.name)
|
||||||
viewModel.onNameError()
|
|
||||||
.toViewError(this, inputName)
|
// Tags
|
||||||
|
inputTags.attachLiveData(this, viewModel.tags)
|
||||||
|
|
||||||
// Url
|
// Url
|
||||||
inputUrl.attachLiveData(this, viewModel.url)
|
inputUrl.attachLiveData(this, viewModel.url)
|
||||||
viewModel.onUrlError()
|
|
||||||
.toViewError(this, inputUrl)
|
|
||||||
viewModel.onUrlWarningVisibility()
|
viewModel.onUrlWarningVisibility()
|
||||||
.toViewVisibility(this, textUrlWarning)
|
.toViewVisibility(this, textUrlWarning)
|
||||||
|
|
||||||
// Timeout
|
// Timeout
|
||||||
responseTimeoutInput.attachLiveData(this, viewModel.timeout)
|
responseTimeoutInput.attachLiveData(this, viewModel.timeout)
|
||||||
viewModel.onTimeoutError()
|
|
||||||
.toViewError(this, responseTimeoutInput)
|
|
||||||
|
|
||||||
// Validation mode
|
// Validation mode
|
||||||
responseValidationMode.attachLiveData(
|
responseValidationMode.attachLiveData(
|
||||||
|
@ -81,8 +105,6 @@ class AddSiteActivity : DarkModeSwitchActivity() {
|
||||||
outTransformer = { ValidationMode.fromIndex(it) },
|
outTransformer = { ValidationMode.fromIndex(it) },
|
||||||
inTransformer = { it.toIndex() }
|
inTransformer = { it.toIndex() }
|
||||||
)
|
)
|
||||||
viewModel.onValidationSearchTermError()
|
|
||||||
.toViewError(this, responseValidationSearchTerm)
|
|
||||||
viewModel.onValidationModeDescription()
|
viewModel.onValidationModeDescription()
|
||||||
.toViewText(this, validationModeDescription)
|
.toViewText(this, validationModeDescription)
|
||||||
|
|
||||||
|
@ -95,30 +117,19 @@ class AddSiteActivity : DarkModeSwitchActivity() {
|
||||||
viewModel.onValidationSearchTermVisibility()
|
viewModel.onValidationSearchTermVisibility()
|
||||||
.toViewVisibility(this, responseValidationSearchTerm)
|
.toViewVisibility(this, responseValidationSearchTerm)
|
||||||
|
|
||||||
// Validation script
|
// SSL certificate
|
||||||
scriptInputLayout.attach(
|
sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it }
|
||||||
codeData = viewModel.validationScript,
|
viewModel.certificateUri.distinct()
|
||||||
errorData = viewModel.onValidationScriptError(),
|
.observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) })
|
||||||
visibility = viewModel.onValidationScriptVisibility()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check interval
|
// Headers
|
||||||
checkIntervalLayout.attach(
|
headersLayout.attach(viewModel.headers)
|
||||||
valueData = viewModel.checkIntervalValue,
|
|
||||||
multiplierData = viewModel.checkIntervalUnit,
|
|
||||||
errorData = viewModel.onCheckIntervalError()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Retry Policy
|
|
||||||
retryPolicyLayout.attach(
|
|
||||||
timesData = viewModel.retryPolicyTimes,
|
|
||||||
minutesData = viewModel.retryPolicyMinutes
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUi() {
|
private fun setupUi() {
|
||||||
toolbarTitle.setText(R.string.add_site)
|
toolbarTitle.setText(R.string.add_site)
|
||||||
toolbar.run {
|
toolbar.run {
|
||||||
|
inflateMenu(R.menu.menu_addsite)
|
||||||
setNavigationIcon(R.drawable.ic_action_close)
|
setNavigationIcon(R.drawable.ic_action_close)
|
||||||
setNavigationOnClickListener { finish() }
|
setNavigationOnClickListener { finish() }
|
||||||
}
|
}
|
||||||
|
@ -131,12 +142,94 @@ class AddSiteActivity : DarkModeSwitchActivity() {
|
||||||
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
||||||
responseValidationMode.adapter = validationOptionsAdapter
|
responseValidationMode.adapter = validationOptionsAdapter
|
||||||
|
|
||||||
// Done button
|
scrollView.onScroll {
|
||||||
doneBtn.setOnClickListener {
|
appToolbar.elevation = if (it > appToolbar.measuredHeight / 2) {
|
||||||
viewModel.commit {
|
appToolbar.dimenFloat(R.dimen.default_elevation)
|
||||||
setResult(RESULT_OK)
|
} else {
|
||||||
finish()
|
0f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSL certificate
|
||||||
|
sslCertificateBrowse.setOnClickListener {
|
||||||
|
val intent = Intent(ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(CATEGORY_OPENABLE)
|
||||||
|
type = "*/*"
|
||||||
|
}
|
||||||
|
startActivityForResult(intent, SELECT_CERT_FILE_RQ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupValidation() {
|
||||||
|
validationForm = form {
|
||||||
|
input(inputName, name = "Name") {
|
||||||
|
isNotEmpty().description(R.string.please_enter_name)
|
||||||
|
}
|
||||||
|
input(inputUrl, name = "URL") {
|
||||||
|
isNotEmpty().description(R.string.please_enter_url)
|
||||||
|
isUrl().description(R.string.please_enter_valid_url)
|
||||||
|
}
|
||||||
|
input(responseTimeoutInput, name = "Timeout", optional = true) {
|
||||||
|
isNumber().greaterThan(0)
|
||||||
|
.description(R.string.please_enter_networkTimeout)
|
||||||
|
}
|
||||||
|
input(responseValidationSearchTerm, name = "Search term") {
|
||||||
|
conditional(responseValidationSearchTerm.isVisibleCondition()) {
|
||||||
|
isNotEmpty().description(R.string.please_enter_search_term)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input(sslCertificateInput, name = "Certificate Path", optional = true) {
|
||||||
|
isUri().hasScheme("file", "content")
|
||||||
|
.that { it.host != null }
|
||||||
|
.description(R.string.please_enter_validCertUri)
|
||||||
|
}
|
||||||
|
submitWith(toolbar.menu, R.id.commit) {
|
||||||
|
viewModel.commit {
|
||||||
|
setResult(RESULT_OK)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation script
|
||||||
|
scriptInputLayout.attach(
|
||||||
|
codeData = viewModel.validationScript,
|
||||||
|
visibility = viewModel.onValidationScriptVisibility(),
|
||||||
|
form = validationForm
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check interval
|
||||||
|
checkIntervalLayout.attach(
|
||||||
|
valueData = viewModel.checkIntervalValue,
|
||||||
|
multiplierData = viewModel.checkIntervalUnit,
|
||||||
|
form = validationForm
|
||||||
|
)
|
||||||
|
|
||||||
|
// Retry Policy
|
||||||
|
retryPolicyLayout.attach(
|
||||||
|
timesData = viewModel.retryPolicyTimes,
|
||||||
|
minutesData = viewModel.retryPolicyMinutes,
|
||||||
|
form = validationForm
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {
|
||||||
|
appToolbar.dimenFloat(R.dimen.default_elevation)
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(
|
||||||
|
requestCode: Int,
|
||||||
|
resultCode: Int,
|
||||||
|
resultData: Intent?
|
||||||
|
) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, resultData)
|
||||||
|
if (requestCode == SELECT_CERT_FILE_RQ && resultCode == RESULT_OK) {
|
||||||
|
sslCertificateInput.setText(resultData?.data?.toString() ?: "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,8 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
import androidx.lifecycle.OnLifecycleEvent
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.AppDatabase
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.data.RetryPolicy
|
import com.afollestad.nocknock.data.model.Header
|
||||||
|
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.SiteSettings
|
import com.afollestad.nocknock.data.model.SiteSettings
|
||||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||||
|
@ -35,11 +36,10 @@ import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||||
import com.afollestad.nocknock.data.model.ValidationResult
|
import com.afollestad.nocknock.data.model.ValidationResult
|
||||||
import com.afollestad.nocknock.data.putSite
|
import com.afollestad.nocknock.data.putSite
|
||||||
import com.afollestad.nocknock.engine.validation.ValidationManager
|
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||||
import com.afollestad.nocknock.ui.ScopedViewModel
|
import com.afollestad.nocknock.ui.ScopedViewModel
|
||||||
import com.afollestad.nocknock.utilities.ext.MINUTE
|
import com.afollestad.nocknock.utilities.ext.MINUTE
|
||||||
import com.afollestad.nocknock.utilities.livedata.map
|
import com.afollestad.nocknock.utilities.livedata.map
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -49,13 +49,14 @@ import java.lang.System.currentTimeMillis
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class AddSiteViewModel(
|
class AddSiteViewModel(
|
||||||
private val database: AppDatabase,
|
private val database: AppDatabase,
|
||||||
private val validationManager: ValidationManager,
|
private val validationManager: ValidationExecutor,
|
||||||
mainDispatcher: CoroutineDispatcher,
|
mainDispatcher: CoroutineDispatcher,
|
||||||
private val ioDispatcher: CoroutineDispatcher
|
private val ioDispatcher: CoroutineDispatcher
|
||||||
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
|
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
|
||||||
|
|
||||||
// Public properties
|
// Public properties
|
||||||
val name = MutableLiveData<String>()
|
val name = MutableLiveData<String>()
|
||||||
|
val tags = MutableLiveData<String>()
|
||||||
val url = MutableLiveData<String>()
|
val url = MutableLiveData<String>()
|
||||||
val timeout = MutableLiveData<Int>()
|
val timeout = MutableLiveData<Int>()
|
||||||
val validationMode = MutableLiveData<ValidationMode>()
|
val validationMode = MutableLiveData<ValidationMode>()
|
||||||
|
@ -65,6 +66,8 @@ class AddSiteViewModel(
|
||||||
val checkIntervalUnit = MutableLiveData<Long>()
|
val checkIntervalUnit = MutableLiveData<Long>()
|
||||||
val retryPolicyTimes = MutableLiveData<Int>()
|
val retryPolicyTimes = MutableLiveData<Int>()
|
||||||
val retryPolicyMinutes = MutableLiveData<Int>()
|
val retryPolicyMinutes = MutableLiveData<Int>()
|
||||||
|
val headers = MutableLiveData<List<Header>>()
|
||||||
|
val certificateUri = MutableLiveData<String>()
|
||||||
|
|
||||||
@OnLifecycleEvent(ON_START)
|
@OnLifecycleEvent(ON_START)
|
||||||
fun setDefaults() {
|
fun setDefaults() {
|
||||||
|
@ -74,24 +77,14 @@ class AddSiteViewModel(
|
||||||
checkIntervalUnit.value = MINUTE
|
checkIntervalUnit.value = MINUTE
|
||||||
retryPolicyMinutes.value = 0
|
retryPolicyMinutes.value = 0
|
||||||
retryPolicyMinutes.value = 0
|
retryPolicyMinutes.value = 0
|
||||||
|
tags.value = ""
|
||||||
|
headers.value = emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private properties
|
|
||||||
private val isLoading = MutableLiveData<Boolean>()
|
private val isLoading = MutableLiveData<Boolean>()
|
||||||
private val nameError = MutableLiveData<Int?>()
|
|
||||||
private val urlError = MutableLiveData<Int?>()
|
|
||||||
private val timeoutError = MutableLiveData<Int?>()
|
|
||||||
private val validationSearchTermError = MutableLiveData<Int?>()
|
|
||||||
private val validationScriptError = MutableLiveData<Int?>()
|
|
||||||
private val checkIntervalValueError = MutableLiveData<Int?>()
|
|
||||||
|
|
||||||
// Expose private properties or calculated properties
|
|
||||||
@CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading
|
@CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading
|
||||||
|
|
||||||
@CheckResult fun onNameError(): LiveData<Int?> = nameError
|
|
||||||
|
|
||||||
@CheckResult fun onUrlError(): LiveData<Int?> = urlError
|
|
||||||
|
|
||||||
@CheckResult fun onUrlWarningVisibility(): LiveData<Boolean> {
|
@CheckResult fun onUrlWarningVisibility(): LiveData<Boolean> {
|
||||||
return url.map {
|
return url.map {
|
||||||
val parsed = HttpUrl.parse(it)
|
val parsed = HttpUrl.parse(it)
|
||||||
|
@ -99,8 +92,6 @@ class AddSiteViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@CheckResult fun onTimeoutError(): LiveData<Int?> = timeoutError
|
|
||||||
|
|
||||||
@CheckResult fun onValidationModeDescription(): LiveData<Int> {
|
@CheckResult fun onValidationModeDescription(): LiveData<Int> {
|
||||||
return validationMode.map {
|
return validationMode.map {
|
||||||
when (it!!) {
|
when (it!!) {
|
||||||
|
@ -111,17 +102,9 @@ class AddSiteViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@CheckResult fun onValidationSearchTermError(): LiveData<Int?> = validationSearchTermError
|
@CheckResult fun onValidationSearchTermVisibility() = validationMode.map { it == TERM_SEARCH }
|
||||||
|
|
||||||
@CheckResult fun onValidationSearchTermVisibility() =
|
@CheckResult fun onValidationScriptVisibility() = validationMode.map { it == JAVASCRIPT }
|
||||||
validationMode.map { it == TERM_SEARCH }
|
|
||||||
|
|
||||||
@CheckResult fun onValidationScriptError(): LiveData<Int?> = validationScriptError
|
|
||||||
|
|
||||||
@CheckResult fun onValidationScriptVisibility() =
|
|
||||||
validationMode.map { it == JAVASCRIPT }
|
|
||||||
|
|
||||||
@CheckResult fun onCheckIntervalError(): LiveData<Int?> = checkIntervalValueError
|
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
fun commit(done: () -> Unit) {
|
fun commit(done: () -> Unit) {
|
||||||
|
@ -132,7 +115,7 @@ class AddSiteViewModel(
|
||||||
val storedModel = withContext(ioDispatcher) {
|
val storedModel = withContext(ioDispatcher) {
|
||||||
database.putSite(newModel)
|
database.putSite(newModel)
|
||||||
}
|
}
|
||||||
validationManager.scheduleCheck(
|
validationManager.scheduleValidation(
|
||||||
site = storedModel,
|
site = storedModel,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true
|
cancelPrevious = true
|
||||||
|
@ -161,75 +144,16 @@ class AddSiteViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateDbModel(): Site? {
|
private fun generateDbModel(): Site? {
|
||||||
var errorCount = 0
|
val timeout = timeout.value ?: 10_000
|
||||||
|
val cleanedTags = tags.value?.split(',')?.joinToString { it.trim() } ?: ""
|
||||||
// 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(
|
val newSettings = SiteSettings(
|
||||||
validationIntervalMs = getCheckIntervalMs(),
|
validationIntervalMs = getCheckIntervalMs(),
|
||||||
validationMode = validationMode.value!!,
|
validationMode = validationMode.value!!,
|
||||||
validationArgs = getValidationArgs(),
|
validationArgs = getValidationArgs(),
|
||||||
networkTimeout = timeout.value!!,
|
networkTimeout = timeout,
|
||||||
disabled = false
|
disabled = false,
|
||||||
|
certificate = certificateUri.value?.toString()
|
||||||
)
|
)
|
||||||
|
|
||||||
val newLastResult = ValidationResult(
|
val newLastResult = ValidationResult(
|
||||||
|
@ -241,7 +165,10 @@ class AddSiteViewModel(
|
||||||
val retryPolicyTimes = retryPolicyTimes.value ?: 0
|
val retryPolicyTimes = retryPolicyTimes.value ?: 0
|
||||||
val retryPolicyMinutes = retryPolicyMinutes.value ?: 0
|
val retryPolicyMinutes = retryPolicyMinutes.value ?: 0
|
||||||
val newRetryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) {
|
val newRetryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) {
|
||||||
RetryPolicy(count = retryPolicyTimes, minutes = retryPolicyMinutes)
|
RetryPolicy(
|
||||||
|
count = retryPolicyTimes,
|
||||||
|
minutes = retryPolicyMinutes
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -250,9 +177,11 @@ class AddSiteViewModel(
|
||||||
id = 0,
|
id = 0,
|
||||||
name = name.value!!.trim(),
|
name = name.value!!.trim(),
|
||||||
url = url.value!!.trim(),
|
url = url.value!!.trim(),
|
||||||
|
tags = cleanedTags,
|
||||||
settings = newSettings,
|
settings = newSettings,
|
||||||
lastResult = newLastResult,
|
lastResult = newLastResult,
|
||||||
retryPolicy = newRetryPolicy
|
retryPolicy = newRetryPolicy,
|
||||||
|
headers = headers.value ?: emptyList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
/**
|
||||||
|
* 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,20 +21,19 @@ import androidx.lifecycle.Observer
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
|
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.list.listItems
|
import com.afollestad.materialdialogs.list.listItems
|
||||||
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.adapter.SiteAdapter
|
import com.afollestad.nocknock.adapter.SiteAdapter
|
||||||
|
import com.afollestad.nocknock.adapter.TagAdapter
|
||||||
import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver
|
import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.dialogs.AboutDialog
|
import com.afollestad.nocknock.dialogs.AboutDialog
|
||||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
||||||
|
import com.afollestad.nocknock.ui.NightMode.UNKNOWN
|
||||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||||
import com.afollestad.nocknock.utilities.ui.toast
|
|
||||||
import com.afollestad.nocknock.viewUrl
|
|
||||||
import com.afollestad.nocknock.viewUrlWithApp
|
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||||
import kotlinx.android.synthetic.main.activity_main.fab
|
import kotlinx.android.synthetic.main.activity_main.fab
|
||||||
import kotlinx.android.synthetic.main.activity_main.list
|
import kotlinx.android.synthetic.main.activity_main.list
|
||||||
|
@ -43,6 +42,7 @@ import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||||
import kotlinx.android.synthetic.main.include_empty_view.emptyText
|
import kotlinx.android.synthetic.main.include_empty_view.emptyText
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
import kotlinx.android.synthetic.main.activity_main.tags_list as tagsList
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class MainActivity : DarkModeSwitchActivity() {
|
class MainActivity : DarkModeSwitchActivity() {
|
||||||
|
@ -53,6 +53,7 @@ class MainActivity : DarkModeSwitchActivity() {
|
||||||
internal val viewModel by viewModel<MainViewModel>()
|
internal val viewModel by viewModel<MainViewModel>()
|
||||||
|
|
||||||
private lateinit var siteAdapter: SiteAdapter
|
private lateinit var siteAdapter: SiteAdapter
|
||||||
|
private lateinit var tagAdapter: TagAdapter
|
||||||
|
|
||||||
private val statusUpdateReceiver by lazy {
|
private val statusUpdateReceiver by lazy {
|
||||||
StatusUpdateIntentReceiver(application, intentProvider) {
|
StatusUpdateIntentReceiver(application, intentProvider) {
|
||||||
|
@ -76,6 +77,10 @@ class MainActivity : DarkModeSwitchActivity() {
|
||||||
.observe(this, Observer { siteAdapter.set(it) })
|
.observe(this, Observer { siteAdapter.set(it) })
|
||||||
viewModel.onEmptyTextVisibility()
|
viewModel.onEmptyTextVisibility()
|
||||||
.toViewVisibility(this, emptyText)
|
.toViewVisibility(this, emptyText)
|
||||||
|
viewModel.onTags()
|
||||||
|
.observe(this, Observer { tagAdapter.set(it) })
|
||||||
|
viewModel.onTagsListVisibility()
|
||||||
|
.toViewVisibility(this, tagsList)
|
||||||
loadingProgress.observe(this, viewModel.onIsLoading())
|
loadingProgress.observe(this, viewModel.onIsLoading())
|
||||||
|
|
||||||
processIntent(intent)
|
processIntent(intent)
|
||||||
|
@ -85,24 +90,35 @@ class MainActivity : DarkModeSwitchActivity() {
|
||||||
toolbar.run {
|
toolbar.run {
|
||||||
inflateMenu(R.menu.menu_main)
|
inflateMenu(R.menu.menu_main)
|
||||||
menu.findItem(R.id.dark_mode)
|
menu.findItem(R.id.dark_mode)
|
||||||
.isChecked = isDarkMode()
|
.apply {
|
||||||
|
if (getCurrentNightMode() == UNKNOWN) {
|
||||||
|
isChecked = isDarkMode()
|
||||||
|
} else {
|
||||||
|
isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
setOnMenuItemClickListener { item ->
|
setOnMenuItemClickListener { item ->
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.about -> AboutDialog.show(this@MainActivity)
|
R.id.about -> AboutDialog.show(this@MainActivity)
|
||||||
R.id.dark_mode -> toggleDarkMode()
|
R.id.dark_mode -> toggleDarkMode()
|
||||||
R.id.support_me -> supportMe()
|
|
||||||
}
|
}
|
||||||
return@setOnMenuItemClickListener true
|
return@setOnMenuItemClickListener true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
siteAdapter = SiteAdapter(this::onSiteSelected)
|
siteAdapter = SiteAdapter(this::onSiteSelected)
|
||||||
|
|
||||||
list.run {
|
list.run {
|
||||||
layoutManager = LinearLayoutManager(this@MainActivity)
|
layoutManager = LinearLayoutManager(this@MainActivity)
|
||||||
adapter = siteAdapter
|
adapter = siteAdapter
|
||||||
addItemDecoration(DividerItemDecoration(this@MainActivity, VERTICAL))
|
addItemDecoration(DividerItemDecoration(this@MainActivity, VERTICAL))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tagAdapter = TagAdapter(viewModel::onTagSelection)
|
||||||
|
tagsList.run {
|
||||||
|
layoutManager = LinearLayoutManager(this@MainActivity, HORIZONTAL, false)
|
||||||
|
adapter = tagAdapter
|
||||||
|
}
|
||||||
|
|
||||||
fab.setOnClickListener { addSite() }
|
fab.setOnClickListener { addSite() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +137,8 @@ class MainActivity : DarkModeSwitchActivity() {
|
||||||
listItems(R.array.site_long_options) { _, i, _ ->
|
listItems(R.array.site_long_options) { _, i, _ ->
|
||||||
when (i) {
|
when (i) {
|
||||||
0 -> viewModel.refreshSite(model)
|
0 -> viewModel.refreshSite(model)
|
||||||
1 -> maybeRemoveSite(model)
|
1 -> addSiteForDuplication(model)
|
||||||
|
2 -> maybeRemoveSite(model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,20 +146,4 @@ class MainActivity : DarkModeSwitchActivity() {
|
||||||
viewSite(model)
|
viewSite(model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun supportMe() {
|
|
||||||
MaterialDialog(this).show {
|
|
||||||
title(R.string.support_me)
|
|
||||||
message(R.string.support_me_message, html = true, lineHeightMultiplier = 1.4f)
|
|
||||||
listItemsSingleChoice(R.array.donation_options) { _, index, _ ->
|
|
||||||
when (index) {
|
|
||||||
0 -> viewUrl("https://paypal.me/AidanFollestad")
|
|
||||||
1 -> viewUrlWithApp("https://cash.me/\$afollestad", pkg = "com.squareup.cash")
|
|
||||||
2 -> viewUrlWithApp("https://venmo.com/afollestad", pkg = "com.venmo")
|
|
||||||
}
|
|
||||||
toast(R.string.thank_you)
|
|
||||||
}
|
|
||||||
positiveButton(R.string.next)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,10 +28,23 @@ import com.afollestad.nocknock.utilities.providers.RealIntentProvider.Companion.
|
||||||
internal const val VIEW_SITE_RQ = 6923
|
internal const val VIEW_SITE_RQ = 6923
|
||||||
internal const val ADD_SITE_RQ = 6969
|
internal const val ADD_SITE_RQ = 6969
|
||||||
|
|
||||||
|
// ADD
|
||||||
|
|
||||||
internal fun MainActivity.addSite() {
|
internal fun MainActivity.addSite() {
|
||||||
startActivityForResult(Intent(this, AddSiteActivity::class.java), ADD_SITE_RQ)
|
startActivityForResult(intentToAdd(), 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) {
|
internal fun MainActivity.viewSite(model: Site) {
|
||||||
startActivityForResult(intentToView(model), VIEW_SITE_RQ)
|
startActivityForResult(intentToView(model), VIEW_SITE_RQ)
|
||||||
}
|
}
|
||||||
|
@ -41,6 +54,8 @@ private fun MainActivity.intentToView(model: Site) =
|
||||||
putExtra(KEY_SITE, model)
|
putExtra(KEY_SITE, model)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MISC
|
||||||
|
|
||||||
internal fun MainActivity.maybeRemoveSite(model: Site) {
|
internal fun MainActivity.maybeRemoveSite(model: Site) {
|
||||||
MaterialDialog(this).show {
|
MaterialDialog(this).show {
|
||||||
title(R.string.remove_site)
|
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.allSites
|
||||||
import com.afollestad.nocknock.data.deleteSite
|
import com.afollestad.nocknock.data.deleteSite
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.engine.validation.ValidationManager
|
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.ui.ScopedViewModel
|
import com.afollestad.nocknock.ui.ScopedViewModel
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
@ -36,7 +36,7 @@ import kotlinx.coroutines.withContext
|
||||||
class MainViewModel(
|
class MainViewModel(
|
||||||
private val database: AppDatabase,
|
private val database: AppDatabase,
|
||||||
private val notificationManager: NockNotificationManager,
|
private val notificationManager: NockNotificationManager,
|
||||||
private val validationManager: ValidationManager,
|
private val validationManager: ValidationExecutor,
|
||||||
mainDispatcher: CoroutineDispatcher,
|
mainDispatcher: CoroutineDispatcher,
|
||||||
private val ioDispatcher: CoroutineDispatcher
|
private val ioDispatcher: CoroutineDispatcher
|
||||||
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
|
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
|
||||||
|
@ -44,6 +44,8 @@ class MainViewModel(
|
||||||
private val sites = MutableLiveData<List<Site>>()
|
private val sites = MutableLiveData<List<Site>>()
|
||||||
private val isLoading = MutableLiveData<Boolean>()
|
private val isLoading = MutableLiveData<Boolean>()
|
||||||
private val emptyTextVisibility = 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
|
@CheckResult fun onSites(): LiveData<List<Site>> = sites
|
||||||
|
|
||||||
|
@ -51,8 +53,14 @@ class MainViewModel(
|
||||||
|
|
||||||
@CheckResult fun onEmptyTextVisibility(): LiveData<Boolean> = emptyTextVisibility
|
@CheckResult fun onEmptyTextVisibility(): LiveData<Boolean> = emptyTextVisibility
|
||||||
|
|
||||||
|
@CheckResult fun onTags(): LiveData<List<String>> = tags
|
||||||
|
|
||||||
|
@CheckResult fun onTagsListVisibility(): LiveData<Boolean> = tagsListVisibility
|
||||||
|
|
||||||
@OnLifecycleEvent(ON_RESUME)
|
@OnLifecycleEvent(ON_RESUME)
|
||||||
fun onResume() = loadSites()
|
fun onResume() = loadSites(emptyList())
|
||||||
|
|
||||||
|
fun onTagSelection(tags: List<String>) = loadSites(tags)
|
||||||
|
|
||||||
fun postSiteUpdate(model: Site) {
|
fun postSiteUpdate(model: Site) {
|
||||||
val currentSites = sites.value ?: return
|
val currentSites = sites.value ?: return
|
||||||
|
@ -65,7 +73,7 @@ class MainViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshSite(model: Site) {
|
fun refreshSite(model: Site) {
|
||||||
validationManager.scheduleCheck(
|
validationManager.scheduleValidation(
|
||||||
site = model,
|
site = model,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true
|
cancelPrevious = true
|
||||||
|
@ -73,7 +81,7 @@ class MainViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeSite(model: Site) {
|
fun removeSite(model: Site) {
|
||||||
validationManager.cancelCheck(model)
|
validationManager.cancelScheduledValidation(model)
|
||||||
notificationManager.cancelStatusNotification(model)
|
notificationManager.cancelStatusNotification(model)
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@ -94,27 +102,56 @@ class MainViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadSites() {
|
private fun loadSites(forTags: List<String>) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
notificationManager.cancelStatusNotifications()
|
notificationManager.cancelStatusNotifications()
|
||||||
sites.value = listOf()
|
|
||||||
emptyTextVisibility.value = false
|
emptyTextVisibility.value = false
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|
||||||
val result = withContext(ioDispatcher) {
|
val unfiltered = withContext(ioDispatcher) {
|
||||||
database.allSites()
|
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
|
sites.value = result
|
||||||
ensureCheckJobs()
|
ensureCheckJobs()
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
emptyTextVisibility.value = result.isEmpty()
|
emptyTextVisibility.value = result.isEmpty()
|
||||||
|
|
||||||
|
val tagsValues = pullOutTags(unfiltered)
|
||||||
|
tags.value = tagsValues
|
||||||
|
tagsListVisibility.value = tagsValues.isNotEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun ensureCheckJobs() {
|
private suspend fun ensureCheckJobs() {
|
||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
validationManager.ensureScheduledChecks()
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@ package com.afollestad.nocknock.ui.viewsite
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.Intent.ACTION_OPEN_DOCUMENT
|
||||||
|
import android.content.Intent.CATEGORY_OPENABLE
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
@ -25,18 +27,23 @@ import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode
|
import com.afollestad.nocknock.data.model.ValidationMode
|
||||||
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
||||||
|
import com.afollestad.nocknock.utilities.ext.onTextChanged
|
||||||
|
import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
|
||||||
|
import com.afollestad.nocknock.utilities.livedata.distinct
|
||||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
|
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
|
||||||
|
import com.afollestad.nocknock.viewcomponents.ext.isVisibleCondition
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.onScroll
|
import com.afollestad.nocknock.viewcomponents.ext.onScroll
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
|
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
|
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||||
|
import com.afollestad.vvalidator.form
|
||||||
|
import com.afollestad.vvalidator.form.Form
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalLayout
|
import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalLayout
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.disableChecksButton
|
import kotlinx.android.synthetic.main.activity_viewsite.headersLayout
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.doneBtn
|
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.iconStatus
|
import kotlinx.android.synthetic.main.activity_viewsite.iconStatus
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.inputName
|
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.inputUrl
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.loadingProgress
|
import kotlinx.android.synthetic.main.activity_viewsite.loadingProgress
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.responseTimeoutInput
|
import kotlinx.android.synthetic.main.activity_viewsite.responseTimeoutInput
|
||||||
|
@ -45,6 +52,8 @@ import kotlinx.android.synthetic.main.activity_viewsite.responseValidationSearch
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.retryPolicyLayout
|
import kotlinx.android.synthetic.main.activity_viewsite.retryPolicyLayout
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.scriptInputLayout
|
import kotlinx.android.synthetic.main.activity_viewsite.scriptInputLayout
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.scrollView
|
import kotlinx.android.synthetic.main.activity_viewsite.scrollView
|
||||||
|
import kotlinx.android.synthetic.main.activity_viewsite.sslCertificateBrowse
|
||||||
|
import kotlinx.android.synthetic.main.activity_viewsite.sslCertificateInput
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.textLastCheckResult
|
import kotlinx.android.synthetic.main.activity_viewsite.textLastCheckResult
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck
|
import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
|
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
|
||||||
|
@ -52,12 +61,17 @@ import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescriptio
|
||||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
import kotlinx.android.synthetic.main.include_app_bar.app_toolbar as appToolbar
|
||||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
|
import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class ViewSiteActivity : DarkModeSwitchActivity() {
|
class ViewSiteActivity : DarkModeSwitchActivity() {
|
||||||
|
companion object {
|
||||||
|
private const val SELECT_CERT_FILE_RQ = 23
|
||||||
|
}
|
||||||
|
|
||||||
internal val viewModel by viewModel<ViewSiteViewModel>()
|
internal val viewModel by viewModel<ViewSiteViewModel>()
|
||||||
|
private lateinit var validationForm: Form
|
||||||
|
|
||||||
private val intentProvider by inject<IntentProvider>()
|
private val intentProvider by inject<IntentProvider>()
|
||||||
private val statusUpdateReceiver by lazy {
|
private val statusUpdateReceiver by lazy {
|
||||||
|
@ -70,17 +84,18 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_viewsite)
|
setContentView(R.layout.activity_viewsite)
|
||||||
setupUi()
|
|
||||||
|
|
||||||
lifecycle.run {
|
|
||||||
addObserver(viewModel)
|
|
||||||
addObserver(statusUpdateReceiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate view model with initial data
|
// Populate view model with initial data
|
||||||
val model = intent.getSerializableExtra(KEY_SITE) as Site
|
val model = intent.getSerializableExtra(KEY_SITE) as Site
|
||||||
viewModel.setModel(model)
|
viewModel.setModel(model)
|
||||||
|
|
||||||
|
setupUi()
|
||||||
|
setupValidation()
|
||||||
|
lifecycle.run {
|
||||||
|
addObserver(viewModel)
|
||||||
|
addObserver(statusUpdateReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
// Loading
|
// Loading
|
||||||
loadingProgress.observe(this, viewModel.onIsLoading())
|
loadingProgress.observe(this, viewModel.onIsLoading())
|
||||||
|
|
||||||
|
@ -92,20 +107,17 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
|
||||||
|
|
||||||
// Name
|
// Name
|
||||||
inputName.attachLiveData(this, viewModel.name)
|
inputName.attachLiveData(this, viewModel.name)
|
||||||
viewModel.onNameError()
|
|
||||||
.toViewError(this, inputName)
|
// Tags
|
||||||
|
inputTags.attachLiveData(this, viewModel.tags)
|
||||||
|
|
||||||
// Url
|
// Url
|
||||||
inputUrl.attachLiveData(this, viewModel.url)
|
inputUrl.attachLiveData(this, viewModel.url)
|
||||||
viewModel.onUrlError()
|
|
||||||
.toViewError(this, inputUrl)
|
|
||||||
viewModel.onUrlWarningVisibility()
|
viewModel.onUrlWarningVisibility()
|
||||||
.toViewVisibility(this, textUrlWarning)
|
.toViewVisibility(this, textUrlWarning)
|
||||||
|
|
||||||
// Timeout
|
// Timeout
|
||||||
responseTimeoutInput.attachLiveData(this, viewModel.timeout)
|
responseTimeoutInput.attachLiveData(this, viewModel.timeout)
|
||||||
viewModel.onTimeoutError()
|
|
||||||
.toViewError(this, responseTimeoutInput)
|
|
||||||
|
|
||||||
// Validation mode
|
// Validation mode
|
||||||
responseValidationMode.attachLiveData(
|
responseValidationMode.attachLiveData(
|
||||||
|
@ -114,8 +126,6 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
|
||||||
outTransformer = { ValidationMode.fromIndex(it) },
|
outTransformer = { ValidationMode.fromIndex(it) },
|
||||||
inTransformer = { it.toIndex() }
|
inTransformer = { it.toIndex() }
|
||||||
)
|
)
|
||||||
viewModel.onValidationSearchTermError()
|
|
||||||
.toViewError(this, responseValidationSearchTerm)
|
|
||||||
viewModel.onValidationModeDescription()
|
viewModel.onValidationModeDescription()
|
||||||
.toViewText(this, validationModeDescription)
|
.toViewText(this, validationModeDescription)
|
||||||
|
|
||||||
|
@ -124,25 +134,13 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
|
||||||
viewModel.onValidationSearchTermVisibility()
|
viewModel.onValidationSearchTermVisibility()
|
||||||
.toViewVisibility(this, responseValidationSearchTerm)
|
.toViewVisibility(this, responseValidationSearchTerm)
|
||||||
|
|
||||||
// Validation script
|
// SSL certificate
|
||||||
scriptInputLayout.attach(
|
sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it }
|
||||||
codeData = viewModel.validationScript,
|
viewModel.certificateUri.distinct()
|
||||||
errorData = viewModel.onValidationScriptError(),
|
.observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) })
|
||||||
visibility = viewModel.onValidationScriptVisibility()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check interval
|
// Headers
|
||||||
checkIntervalLayout.attach(
|
headersLayout.attach(viewModel.headers)
|
||||||
valueData = viewModel.checkIntervalValue,
|
|
||||||
multiplierData = viewModel.checkIntervalUnit,
|
|
||||||
errorData = viewModel.onCheckIntervalError()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Retry Policy
|
|
||||||
retryPolicyLayout.attach(
|
|
||||||
timesData = viewModel.retryPolicyTimes,
|
|
||||||
minutesData = viewModel.retryPolicyMinutes
|
|
||||||
)
|
|
||||||
|
|
||||||
// Last/next check
|
// Last/next check
|
||||||
viewModel.onLastCheckResultText()
|
viewModel.onLastCheckResultText()
|
||||||
|
@ -152,25 +150,30 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUi() {
|
private fun setupUi() {
|
||||||
toolbarTitle.setText(R.string.view_site)
|
toolbarTitle.text = ""
|
||||||
toolbar.run {
|
toolbar.run {
|
||||||
setNavigationIcon(R.drawable.ic_action_close)
|
setNavigationIcon(R.drawable.ic_action_close)
|
||||||
setNavigationOnClickListener { finish() }
|
setNavigationOnClickListener { finish() }
|
||||||
inflateMenu(R.menu.menu_viewsite)
|
inflateMenu(R.menu.menu_viewsite)
|
||||||
|
|
||||||
menu.findItem(R.id.refresh)
|
menu.findItem(R.id.refresh)
|
||||||
.setActionView(R.layout.menu_item_refresh_icon)
|
.setActionView(R.layout.menu_item_refresh_icon)
|
||||||
.apply {
|
.apply {
|
||||||
actionView.setOnClickListener { viewModel.checkNow() }
|
actionView.setOnClickListener { viewModel.checkNow() }
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnMenuItemClickListener {
|
setOnMenuItemClickListener {
|
||||||
maybeRemoveSite()
|
when (it.itemId) {
|
||||||
|
R.id.remove -> maybeRemoveSite()
|
||||||
|
R.id.disableChecks -> maybeDisableChecks()
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollView.onScroll {
|
scrollView.onScroll {
|
||||||
toolbar.elevation = if (it > toolbar.height / 4) {
|
appToolbar.elevation = if (it > appToolbar.measuredHeight / 2) {
|
||||||
toolbar.dimenFloat(R.dimen.default_elevation)
|
appToolbar.dimenFloat(R.dimen.default_elevation)
|
||||||
} else {
|
} else {
|
||||||
0f
|
0f
|
||||||
}
|
}
|
||||||
|
@ -186,14 +189,95 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
|
||||||
|
|
||||||
// Disabled button
|
// Disabled button
|
||||||
viewModel.onDisableChecksVisibility()
|
viewModel.onDisableChecksVisibility()
|
||||||
.toViewVisibility(this, disableChecksButton)
|
.observe(this, Observer {
|
||||||
disableChecksButton.setOnClickListener { maybeDisableChecks() }
|
toolbar.menu.findItem(R.id.disableChecks)
|
||||||
|
.isVisible = it
|
||||||
|
})
|
||||||
|
|
||||||
// Done button
|
// Done item text
|
||||||
viewModel.onDoneButtonText()
|
viewModel.onDoneButtonText()
|
||||||
.toViewText(this, doneBtn)
|
.observe(this, Observer {
|
||||||
doneBtn.setOnClickListener {
|
toolbar.menu.findItem(R.id.commit)
|
||||||
viewModel.commit { finish() }
|
.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() ?: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,9 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.AppDatabase
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.data.RetryPolicy
|
|
||||||
import com.afollestad.nocknock.data.deleteSite
|
import com.afollestad.nocknock.data.deleteSite
|
||||||
|
import com.afollestad.nocknock.data.model.Header
|
||||||
|
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.Status
|
import com.afollestad.nocknock.data.model.Status
|
||||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||||
|
@ -35,14 +36,13 @@ import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||||
import com.afollestad.nocknock.data.model.ValidationResult
|
import com.afollestad.nocknock.data.model.ValidationResult
|
||||||
import com.afollestad.nocknock.data.model.textRes
|
import com.afollestad.nocknock.data.model.textRes
|
||||||
import com.afollestad.nocknock.data.updateSite
|
import com.afollestad.nocknock.data.updateSite
|
||||||
import com.afollestad.nocknock.engine.validation.ValidationManager
|
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.ui.ScopedViewModel
|
import com.afollestad.nocknock.ui.ScopedViewModel
|
||||||
import com.afollestad.nocknock.utilities.ext.formatDate
|
import com.afollestad.nocknock.utilities.ext.formatDate
|
||||||
import com.afollestad.nocknock.utilities.livedata.map
|
import com.afollestad.nocknock.utilities.livedata.map
|
||||||
import com.afollestad.nocknock.utilities.livedata.zip
|
import com.afollestad.nocknock.utilities.livedata.zip
|
||||||
import com.afollestad.nocknock.utilities.providers.StringProvider
|
import com.afollestad.nocknock.utilities.providers.StringProvider
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -54,7 +54,7 @@ class ViewSiteViewModel(
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val database: AppDatabase,
|
private val database: AppDatabase,
|
||||||
private val notificationManager: NockNotificationManager,
|
private val notificationManager: NockNotificationManager,
|
||||||
private val validationManager: ValidationManager,
|
private val validationManager: ValidationExecutor,
|
||||||
mainDispatcher: CoroutineDispatcher,
|
mainDispatcher: CoroutineDispatcher,
|
||||||
private val ioDispatcher: CoroutineDispatcher
|
private val ioDispatcher: CoroutineDispatcher
|
||||||
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
|
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
|
||||||
|
@ -64,6 +64,7 @@ class ViewSiteViewModel(
|
||||||
// Public properties
|
// Public properties
|
||||||
val status = MutableLiveData<Status>()
|
val status = MutableLiveData<Status>()
|
||||||
val name = MutableLiveData<String>()
|
val name = MutableLiveData<String>()
|
||||||
|
val tags = MutableLiveData<String>()
|
||||||
val url = MutableLiveData<String>()
|
val url = MutableLiveData<String>()
|
||||||
val timeout = MutableLiveData<Int>()
|
val timeout = MutableLiveData<Int>()
|
||||||
val validationMode = MutableLiveData<ValidationMode>()
|
val validationMode = MutableLiveData<ValidationMode>()
|
||||||
|
@ -73,25 +74,15 @@ class ViewSiteViewModel(
|
||||||
val checkIntervalUnit = MutableLiveData<Long>()
|
val checkIntervalUnit = MutableLiveData<Long>()
|
||||||
val retryPolicyTimes = MutableLiveData<Int>()
|
val retryPolicyTimes = MutableLiveData<Int>()
|
||||||
val retryPolicyMinutes = MutableLiveData<Int>()
|
val retryPolicyMinutes = MutableLiveData<Int>()
|
||||||
|
val headers = MutableLiveData<List<Header>>()
|
||||||
|
val certificateUri = MutableLiveData<String>()
|
||||||
internal val disabled = MutableLiveData<Boolean>()
|
internal val disabled = MutableLiveData<Boolean>()
|
||||||
internal val lastResult = MutableLiveData<ValidationResult?>()
|
internal val lastResult = MutableLiveData<ValidationResult?>()
|
||||||
|
|
||||||
// Private properties
|
|
||||||
private val isLoading = MutableLiveData<Boolean>()
|
private val isLoading = MutableLiveData<Boolean>()
|
||||||
private val nameError = MutableLiveData<Int?>()
|
|
||||||
private val urlError = MutableLiveData<Int?>()
|
|
||||||
private val timeoutError = MutableLiveData<Int?>()
|
|
||||||
private val validationSearchTermError = MutableLiveData<Int?>()
|
|
||||||
private val validationScriptError = MutableLiveData<Int?>()
|
|
||||||
private val checkIntervalValueError = MutableLiveData<Int?>()
|
|
||||||
|
|
||||||
// Expose private properties or calculated properties
|
|
||||||
@CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading
|
@CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading
|
||||||
|
|
||||||
@CheckResult fun onNameError(): LiveData<Int?> = nameError
|
|
||||||
|
|
||||||
@CheckResult fun onUrlError(): LiveData<Int?> = urlError
|
|
||||||
|
|
||||||
@CheckResult fun onUrlWarningVisibility(): LiveData<Boolean> {
|
@CheckResult fun onUrlWarningVisibility(): LiveData<Boolean> {
|
||||||
return url.map {
|
return url.map {
|
||||||
val parsed = HttpUrl.parse(it)
|
val parsed = HttpUrl.parse(it)
|
||||||
|
@ -99,8 +90,6 @@ class ViewSiteViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@CheckResult fun onTimeoutError(): LiveData<Int?> = timeoutError
|
|
||||||
|
|
||||||
@CheckResult fun onValidationModeDescription(): LiveData<Int> {
|
@CheckResult fun onValidationModeDescription(): LiveData<Int> {
|
||||||
return validationMode.map {
|
return validationMode.map {
|
||||||
when (it!!) {
|
when (it!!) {
|
||||||
|
@ -111,20 +100,11 @@ class ViewSiteViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@CheckResult fun onValidationSearchTermError(): LiveData<Int?> = validationSearchTermError
|
@CheckResult fun onValidationSearchTermVisibility() = validationMode.map { it == TERM_SEARCH }
|
||||||
|
|
||||||
@CheckResult fun onValidationSearchTermVisibility() =
|
@CheckResult fun onValidationScriptVisibility() = validationMode.map { it == JAVASCRIPT }
|
||||||
validationMode.map { it == TERM_SEARCH }
|
|
||||||
|
|
||||||
@CheckResult fun onValidationScriptError(): LiveData<Int?> = validationScriptError
|
@CheckResult fun onDisableChecksVisibility(): LiveData<Boolean> = disabled.map { !it }
|
||||||
|
|
||||||
@CheckResult fun onValidationScriptVisibility() =
|
|
||||||
validationMode.map { it == JAVASCRIPT }
|
|
||||||
|
|
||||||
@CheckResult fun onCheckIntervalError(): LiveData<Int?> = checkIntervalValueError
|
|
||||||
|
|
||||||
@CheckResult fun onDisableChecksVisibility(): LiveData<Boolean> =
|
|
||||||
disabled.map { !it }
|
|
||||||
|
|
||||||
@CheckResult fun onDoneButtonText(): LiveData<Int> =
|
@CheckResult fun onDoneButtonText(): LiveData<Int> =
|
||||||
disabled.map {
|
disabled.map {
|
||||||
|
@ -168,7 +148,7 @@ class ViewSiteViewModel(
|
||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
database.updateSite(updatedModel)
|
database.updateSite(updatedModel)
|
||||||
}
|
}
|
||||||
validationManager.scheduleCheck(
|
validationManager.scheduleValidation(
|
||||||
site = updatedModel,
|
site = updatedModel,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true
|
cancelPrevious = true
|
||||||
|
@ -184,7 +164,7 @@ class ViewSiteViewModel(
|
||||||
status = WAITING
|
status = WAITING
|
||||||
)
|
)
|
||||||
setModel(checkModel)
|
setModel(checkModel)
|
||||||
validationManager.scheduleCheck(
|
validationManager.scheduleValidation(
|
||||||
site = checkModel,
|
site = checkModel,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true
|
cancelPrevious = true
|
||||||
|
@ -192,7 +172,7 @@ class ViewSiteViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeSite(done: () -> Unit) {
|
fun removeSite(done: () -> Unit) {
|
||||||
validationManager.cancelCheck(site)
|
validationManager.cancelScheduledValidation(site)
|
||||||
notificationManager.cancelStatusNotification(site)
|
notificationManager.cancelStatusNotification(site)
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@ -206,7 +186,7 @@ class ViewSiteViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disableSite() {
|
fun disableSite() {
|
||||||
validationManager.cancelCheck(site)
|
validationManager.cancelScheduledValidation(site)
|
||||||
notificationManager.cancelStatusNotification(site)
|
notificationManager.cancelStatusNotification(site)
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@ -242,75 +222,16 @@ class ViewSiteViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUpdatedDbModel(): Site? {
|
private fun getUpdatedDbModel(): Site? {
|
||||||
var errorCount = 0
|
val timeout = timeout.value ?: 10_000
|
||||||
|
val cleanedTags = tags.value?.split(',')?.joinToString(separator = ",") ?: ""
|
||||||
// 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(
|
val newSettings = site.settings!!.copy(
|
||||||
validationIntervalMs = getCheckIntervalMs(),
|
validationIntervalMs = getCheckIntervalMs(),
|
||||||
validationMode = validationMode.value!!,
|
validationMode = validationMode.value!!,
|
||||||
validationArgs = getValidationArgs(),
|
validationArgs = getValidationArgs(),
|
||||||
networkTimeout = timeout.value!!,
|
networkTimeout = timeout,
|
||||||
disabled = false
|
disabled = false,
|
||||||
|
certificate = certificateUri.value?.toString()
|
||||||
)
|
)
|
||||||
|
|
||||||
val retryPolicyTimes = retryPolicyTimes.value ?: 0
|
val retryPolicyTimes = retryPolicyTimes.value ?: 0
|
||||||
|
@ -318,10 +239,16 @@ class ViewSiteViewModel(
|
||||||
val retryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) {
|
val retryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) {
|
||||||
if (site.retryPolicy != null) {
|
if (site.retryPolicy != null) {
|
||||||
// Have existing policy, update it
|
// Have existing policy, update it
|
||||||
site.retryPolicy!!.copy(count = retryPolicyTimes, minutes = retryPolicyMinutes)
|
site.retryPolicy!!.copy(
|
||||||
|
count = retryPolicyTimes,
|
||||||
|
minutes = retryPolicyMinutes
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// Create new policy
|
// Create new policy
|
||||||
RetryPolicy(count = retryPolicyTimes, minutes = retryPolicyMinutes)
|
RetryPolicy(
|
||||||
|
count = retryPolicyTimes,
|
||||||
|
minutes = retryPolicyMinutes
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No policy
|
// No policy
|
||||||
|
@ -330,9 +257,11 @@ class ViewSiteViewModel(
|
||||||
|
|
||||||
return site.copy(
|
return site.copy(
|
||||||
name = name.value!!.trim(),
|
name = name.value!!.trim(),
|
||||||
|
tags = cleanedTags,
|
||||||
url = url.value!!.trim(),
|
url = url.value!!.trim(),
|
||||||
settings = newSettings,
|
settings = newSettings,
|
||||||
retryPolicy = retryPolicy
|
retryPolicy = retryPolicy,
|
||||||
|
headers = headers.value ?: emptyList()
|
||||||
)
|
)
|
||||||
.withStatus(status = WAITING)
|
.withStatus(status = WAITING)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.ui.viewsite
|
package com.afollestad.nocknock.ui.viewsite
|
||||||
|
|
||||||
import com.afollestad.nocknock.data.RetryPolicy
|
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||||
|
@ -32,6 +32,7 @@ fun ViewSiteViewModel.setModel(site: Site) {
|
||||||
|
|
||||||
status.value = site.lastResult?.status ?: WAITING
|
status.value = site.lastResult?.status ?: WAITING
|
||||||
name.value = site.name
|
name.value = site.name
|
||||||
|
tags.value = site.tags
|
||||||
url.value = site.url
|
url.value = site.url
|
||||||
timeout.value = settings.networkTimeout
|
timeout.value = settings.networkTimeout
|
||||||
|
|
||||||
|
@ -53,6 +54,12 @@ fun ViewSiteViewModel.setModel(site: Site) {
|
||||||
|
|
||||||
setCheckInterval(settings.validationIntervalMs)
|
setCheckInterval(settings.validationIntervalMs)
|
||||||
setRetryPolicy(site.retryPolicy)
|
setRetryPolicy(site.retryPolicy)
|
||||||
|
headers.value = site.headers
|
||||||
|
if (settings.certificate == "null") {
|
||||||
|
certificateUri.value = ""
|
||||||
|
} else {
|
||||||
|
certificateUri.value = settings.certificate
|
||||||
|
}
|
||||||
|
|
||||||
this.disabled.value = settings.disabled
|
this.disabled.value = settings.disabled
|
||||||
this.lastResult.value = site.lastResult
|
this.lastResult.value = site.lastResult
|
||||||
|
@ -62,22 +69,22 @@ private fun ViewSiteViewModel.setCheckInterval(interval: Long) {
|
||||||
when {
|
when {
|
||||||
interval >= WEEK -> {
|
interval >= WEEK -> {
|
||||||
checkIntervalValue.value =
|
checkIntervalValue.value =
|
||||||
getIntervalFromUnit(interval, WEEK)
|
getIntervalFromUnit(interval, WEEK)
|
||||||
checkIntervalUnit.value = WEEK
|
checkIntervalUnit.value = WEEK
|
||||||
}
|
}
|
||||||
interval >= DAY -> {
|
interval >= DAY -> {
|
||||||
checkIntervalValue.value =
|
checkIntervalValue.value =
|
||||||
getIntervalFromUnit(interval, DAY)
|
getIntervalFromUnit(interval, DAY)
|
||||||
checkIntervalUnit.value = DAY
|
checkIntervalUnit.value = DAY
|
||||||
}
|
}
|
||||||
interval >= HOUR -> {
|
interval >= HOUR -> {
|
||||||
checkIntervalValue.value =
|
checkIntervalValue.value =
|
||||||
getIntervalFromUnit(interval, HOUR)
|
getIntervalFromUnit(interval, HOUR)
|
||||||
checkIntervalUnit.value = HOUR
|
checkIntervalUnit.value = HOUR
|
||||||
}
|
}
|
||||||
interval >= MINUTE -> {
|
interval >= MINUTE -> {
|
||||||
checkIntervalValue.value =
|
checkIntervalValue.value =
|
||||||
getIntervalFromUnit(interval, MINUTE)
|
getIntervalFromUnit(interval, MINUTE)
|
||||||
checkIntervalUnit.value = MINUTE
|
checkIntervalUnit.value = MINUTE
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
|
5
app/src/main/res/color/unchecked_chip_text.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?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>
|
13
app/src/main/res/drawable/checked_chip.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?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>
|
13
app/src/main/res/drawable/checked_chip_pressed.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?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>
|
5
app/src/main/res/drawable/checked_chip_selector.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?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>
|
10
app/src/main/res/drawable/ic_check.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<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>
|
13
app/src/main/res/drawable/unchecked_chip.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?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>
|
13
app/src/main/res/drawable/unchecked_chip_pressed.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?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>
|
5
app/src/main/res/drawable/unchecked_chip_selector.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?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,6 +16,7 @@
|
||||||
<include layout="@layout/include_app_bar"/>
|
<include layout="@layout/include_app_bar"/>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
android:id="@+id/scrollView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
>
|
>
|
||||||
|
@ -24,59 +25,61 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingBottom="@dimen/content_inset"
|
android:paddingBottom="@dimen/content_inset_double"
|
||||||
android:paddingLeft="@dimen/content_inset"
|
android:paddingLeft="@dimen/content_inset"
|
||||||
android:paddingRight="@dimen/content_inset"
|
android:paddingRight="@dimen/content_inset"
|
||||||
|
android:paddingTop="@dimen/content_inset_half"
|
||||||
>
|
>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<TextView
|
||||||
android:id="@+id/nameTiLayout"
|
android:layout_marginTop="0dp"
|
||||||
android:layout_width="match_parent"
|
android:text="@string/site_name"
|
||||||
android:layout_height="wrap_content"
|
style="@style/InputForm.Header"
|
||||||
android:layout_marginLeft="-4dp"
|
/>
|
||||||
android:layout_marginRight="-4dp"
|
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
|
||||||
>
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<EditText
|
||||||
android:id="@+id/inputName"
|
android:id="@+id/inputName"
|
||||||
android:layout_width="match_parent"
|
android:hint="@string/site_name_hint"
|
||||||
android:layout_height="wrap_content"
|
android:inputType="textPersonName|textCapWords|textAutoCorrect"
|
||||||
android:hint="@string/site_name"
|
android:nextFocusDown="@+id/inputUrl"
|
||||||
android:inputType="textPersonName|textCapWords|textAutoCorrect"
|
tools:ignore="Autofill"
|
||||||
style="@style/NockText.Body"
|
style="@style/InputForm.Field"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
<TextView
|
||||||
|
android:text="@string/site_url"
|
||||||
|
style="@style/InputForm.Header"
|
||||||
|
/>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<EditText
|
||||||
android:id="@+id/urlTiLayout"
|
android:id="@+id/inputUrl"
|
||||||
android:layout_width="match_parent"
|
android:hint="@string/site_url_hint"
|
||||||
android:layout_height="wrap_content"
|
android:inputType="textUri"
|
||||||
android:layout_marginLeft="-4dp"
|
android:nextFocusDown="@+id/inputTags"
|
||||||
android:layout_marginRight="-4dp"
|
tools:ignore="Autofill"
|
||||||
android:layout_marginTop="@dimen/content_inset_half"
|
style="@style/InputForm.Field"
|
||||||
>
|
/>
|
||||||
|
|
||||||
<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
|
<TextView
|
||||||
android:id="@+id/textUrlWarning"
|
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:text="@string/warning_http_url"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
style="@style/NockText.Footnote"
|
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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<include layout="@layout/include_divider"/>
|
<include layout="@layout/include_divider"/>
|
||||||
|
@ -88,35 +91,10 @@
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
android:layout_marginTop="@dimen/content_inset"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/content_inset_less"
|
|
||||||
android:text="@string/response_timeout"
|
|
||||||
style="@style/NockText.SectionHeader"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/responseTimeoutInput"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="-4dp"
|
|
||||||
android:layout_marginStart="-4dp"
|
|
||||||
android:layout_marginTop="@dimen/content_inset_quarter"
|
|
||||||
android:hint="@string/response_timeout_default"
|
|
||||||
android:inputType="number"
|
|
||||||
android:maxLength="8"
|
|
||||||
tools:ignore="Autofill,HardcodedText,LabelFor"
|
|
||||||
style="@style/NockText.Body"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/responseValidationLabel"
|
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"
|
android:text="@string/response_validation_mode"
|
||||||
style="@style/NockText.SectionHeader"
|
style="@style/InputForm.Header"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Spinner
|
<Spinner
|
||||||
|
@ -145,7 +123,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="@dimen/content_inset"
|
android:layout_marginBottom="@dimen/content_inset"
|
||||||
android:layout_marginTop="@dimen/content_inset_half"
|
android:layout_marginTop="@dimen/content_inset_half"
|
||||||
android:background="@color/lighterGray"
|
android:background="?scriptLayoutBackground"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -158,6 +136,8 @@
|
||||||
style="@style/NockText.Body.Light"
|
style="@style/NockText.Body.Light"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<include layout="@layout/include_divider"/>
|
||||||
|
|
||||||
<com.afollestad.nocknock.viewcomponents.retrypolicy.RetryPolicyLayout
|
<com.afollestad.nocknock.viewcomponents.retrypolicy.RetryPolicyLayout
|
||||||
android:id="@+id/retryPolicyLayout"
|
android:id="@+id/retryPolicyLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -165,13 +145,66 @@
|
||||||
android:layout_marginTop="@dimen/content_inset_more"
|
android:layout_marginTop="@dimen/content_inset_more"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<TextView
|
||||||
android:id="@+id/doneBtn"
|
android:layout_marginTop="@dimen/content_inset"
|
||||||
|
android:text="@string/response_timeout"
|
||||||
|
style="@style/InputForm.Header"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/responseTimeoutInput"
|
||||||
|
android:hint="@string/response_timeout_default"
|
||||||
|
android:inputType="number"
|
||||||
|
android:maxLength="8"
|
||||||
|
tools:ignore="Autofill"
|
||||||
|
style="@style/InputForm.Field"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/content_inset"
|
||||||
|
android:text="@string/ssl_certificate"
|
||||||
|
style="@style/NockText.SectionHeader"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/content_inset_double"
|
android:orientation="horizontal"
|
||||||
android:text="@string/add_site"
|
>
|
||||||
style="@style/AccentButton"
|
|
||||||
|
<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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -17,6 +17,19 @@
|
||||||
|
|
||||||
<include layout="@layout/include_app_bar"/>
|
<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
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/list"
|
android:id="@+id/list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
@ -26,15 +26,30 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingBottom="@dimen/content_inset"
|
android:paddingBottom="@dimen/content_inset_double"
|
||||||
android:paddingLeft="@dimen/content_inset"
|
android:paddingLeft="@dimen/content_inset"
|
||||||
android:paddingRight="@dimen/content_inset"
|
android:paddingRight="@dimen/content_inset"
|
||||||
android:paddingTop="@dimen/content_inset"
|
android:paddingTop="@dimen/content_inset_less"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<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
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/content_inset_quarter"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
@ -55,24 +70,13 @@
|
||||||
android:orientation="vertical"
|
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
|
<EditText
|
||||||
android:id="@+id/inputUrl"
|
android:id="@+id/inputUrl"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/site_url"
|
android:hint="@string/site_url"
|
||||||
android:inputType="textUri"
|
android:inputType="textUri"
|
||||||
|
android:nextFocusDown="@+id/inputTags"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:transitionName="site_url"
|
android:transitionName="site_url"
|
||||||
tools:ignore="Autofill,UnusedAttribute"
|
tools:ignore="Autofill,UnusedAttribute"
|
||||||
|
@ -91,6 +95,19 @@
|
||||||
style="@style/NockText.Footnote"
|
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>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -109,35 +126,6 @@
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
android:layout_marginTop="@dimen/content_inset"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
|
||||||
android:text="@string/response_timeout"
|
|
||||||
style="@style/NockText.SectionHeader"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/responseTimeoutInput"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="-4dp"
|
|
||||||
android:layout_marginStart="-4dp"
|
|
||||||
android: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
|
<TextView
|
||||||
android:id="@+id/responseValidationLabel"
|
android:id="@+id/responseValidationLabel"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -173,7 +161,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="@dimen/content_inset"
|
android:layout_marginBottom="@dimen/content_inset"
|
||||||
android:layout_marginTop="@dimen/content_inset_half"
|
android:layout_marginTop="@dimen/content_inset_half"
|
||||||
android:background="@color/lighterGray"
|
android:background="?scriptLayoutBackground"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -186,6 +174,13 @@
|
||||||
style="@style/NockText.Body.Light"
|
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
|
<com.afollestad.nocknock.viewcomponents.retrypolicy.RetryPolicyLayout
|
||||||
android:id="@+id/retryPolicyLayout"
|
android:id="@+id/retryPolicyLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -193,6 +188,76 @@
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
android:layout_marginTop="@dimen/content_inset"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/content_inset"
|
||||||
|
android:text="@string/response_timeout"
|
||||||
|
style="@style/NockText.SectionHeader"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/responseTimeoutInput"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="-4dp"
|
||||||
|
android:layout_marginStart="-4dp"
|
||||||
|
android:layout_marginTop="@dimen/content_inset_quarter"
|
||||||
|
android:hint="@string/response_timeout_default"
|
||||||
|
android:inputType="number"
|
||||||
|
android:maxLength="8"
|
||||||
|
tools:ignore="Autofill,HardcodedText,LabelFor"
|
||||||
|
style="@style/NockText.Body"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/content_inset"
|
||||||
|
android:text="@string/ssl_certificate"
|
||||||
|
style="@style/NockText.SectionHeader"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/sslCertificateInput"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start|center_vertical"
|
||||||
|
android:layout_marginStart="-4dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:hint="@string/ssl_certificate_automatic"
|
||||||
|
android:inputType="textUri"
|
||||||
|
tools:ignore="Autofill,HardcodedText,LabelFor"
|
||||||
|
style="@style/NockText.Body"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/sslCertificateBrowse"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end|center_vertical"
|
||||||
|
android:text="@string/ssl_certificate_browse"
|
||||||
|
style="@style/AccentTextButton"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<include layout="@layout/include_divider"/>
|
||||||
|
|
||||||
|
<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"/>
|
<include layout="@layout/include_divider"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -229,24 +294,6 @@
|
||||||
style="@style/NockText.Body"
|
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>
|
</LinearLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
15
app/src/main/res/layout/list_item_tag.xml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?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"
|
||||||
|
/>
|
9
app/src/main/res/menu/menu_addsite.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?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,7 +7,4 @@
|
||||||
android:id="@+id/dark_mode"
|
android:id="@+id/dark_mode"
|
||||||
android:checkable="true"
|
android:checkable="true"
|
||||||
android:title="@string/dark_mode"/>
|
android:title="@string/dark_mode"/>
|
||||||
<item
|
|
||||||
android:id="@+id/support_me"
|
|
||||||
android:title="@string/support_me"/>
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
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
|
<item
|
||||||
android:id="@+id/refresh"
|
android:id="@+id/refresh"
|
||||||
android:icon="@drawable/ic_action_refresh"
|
android:icon="@drawable/ic_action_refresh"
|
||||||
android:title="@string/refresh_status"
|
android:title="@string/refresh_status"
|
||||||
app:showAsAction="ifRoom"/>
|
app:showAsAction="ifRoom"/>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/remove"
|
android:id="@+id/remove"
|
||||||
android:icon="@drawable/ic_action_delete"
|
android:icon="@drawable/ic_action_delete"
|
||||||
android:title="@string/remove_site"
|
android:title="@string/remove_site"
|
||||||
app:showAsAction="ifRoom"/>
|
app:showAsAction="ifRoom"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/disableChecks"
|
||||||
|
android:title="@string/disable_automatic_checks"
|
||||||
|
/>
|
||||||
</menu>
|
</menu>
|
||||||
|
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 5.4 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 7.7 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 15 KiB |
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
<string-array name="site_long_options" translatable="false">
|
<string-array name="site_long_options" translatable="false">
|
||||||
<item>@string/refresh_status</item>
|
<item>@string/refresh_status</item>
|
||||||
|
<item>@string/duplicate_and_modify</item>
|
||||||
<item>@string/remove_site</item>
|
<item>@string/remove_site</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
@ -12,10 +13,4 @@
|
||||||
<item>JavaScript Evaluation</item>
|
<item>JavaScript Evaluation</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="donation_options">
|
|
||||||
<item>via PayPal</item>
|
|
||||||
<item>via Cash App</item>
|
|
||||||
<item>via Venmo</item>
|
|
||||||
</string-array>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -4,5 +4,6 @@
|
||||||
<attr format="color" name="toolbarTitleColor"/>
|
<attr format="color" name="toolbarTitleColor"/>
|
||||||
<attr format="color" name="dividerColor"/>
|
<attr format="color" name="dividerColor"/>
|
||||||
<attr format="color" name="iconColor"/>
|
<attr format="color" name="iconColor"/>
|
||||||
|
<attr format="color" name="scriptLayoutBackground"/>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -7,7 +7,11 @@
|
||||||
<color name="colorPrimary_darkTheme">#212121</color>
|
<color name="colorPrimary_darkTheme">#212121</color>
|
||||||
<color name="colorPrimaryDark_darkTheme">#252525</color>
|
<color name="colorPrimaryDark_darkTheme">#252525</color>
|
||||||
|
|
||||||
<color name="lighterGray">#303030</color>
|
<color name="darkerGray">#303030</color>
|
||||||
|
<color name="lighterGray">#EEEEEE</color>
|
||||||
|
|
||||||
<color name="colorAccent">#FF6E40</color>
|
<color name="colorAccent">#FF6E40</color>
|
||||||
|
<color name="colorAccent_pressed">#E44615</color>
|
||||||
|
<color name="colorAccent_translucent">#40FF6E40</color>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -2,5 +2,6 @@
|
||||||
|
|
||||||
<dimen name="empty_text_size">28sp</dimen>
|
<dimen name="empty_text_size">28sp</dimen>
|
||||||
<dimen name="list_text_spacing">6dp</dimen>
|
<dimen name="list_text_spacing">6dp</dimen>
|
||||||
|
<dimen name="toolbar_elevation">4dp</dimen>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#758F9A</color>
|
||||||
|
</resources>
|
|
@ -1,35 +1,42 @@
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<string name="app_name">Nock Nock</string>
|
<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="no_sites_added">No sites added!</string>
|
||||||
|
|
||||||
<string name="about">About</string>
|
<string name="about">About</string>
|
||||||
<string name="about_body"><![CDATA[
|
<string name="about_body"><![CDATA[
|
||||||
<b>Nock Nock</b>, a simple app designed by <b>Aidan Follestad</b>.<br/>
|
A simple app designed by <b>Aidan Follestad</b>.<br/>
|
||||||
<a href=\'https://af.codes\'>Website</a>
|
<a href=\'https://af.codes\'>Website</a>
|
||||||
<a href=\'https://twitter.com/afollestad\'>Twitter</a>
|
<a href=\'https://twitter.com/afollestad\'>Twitter</a>
|
||||||
<a href=\'https://github.com/afollestad\'>GitHub</a>
|
<a href=\'https://github.com/afollestad\'>GitHub</a>
|
||||||
<a href=\'https://www.linkedin.com/in/afollestad\'>LinkedIn</a>
|
<a href=\'https://www.linkedin.com/in/afollestad\'>LinkedIn</a>
|
||||||
<br/><br/><i>Nock Nock is open source! Check out the <a href=\'https://github.com/afollestad/nock-nock\'>GitHub page</a>!</i>
|
<br/><br/><i>Nock Nock is open source! Check out the <a href=\'https://github.com/afollestad/nock-nock\'>GitHub page</a>!</i>
|
||||||
<br/>Icon by <a href=\'https://plus.google.com/+KevinAguilarC\'>Kevin Aguilar</a> of <b>221 Pixels</b>.
|
<br/>Icon by <a href=\'https://plus.google.com/+KevinAguilarC\'>Kevin Aguilar</a> of <b>221 Pixels</b>.
|
||||||
|
<br/>View the <a href=\'https://af.codes/privacypolicies/nocknock.html\'>Privacy Policy</a>.
|
||||||
]]></string>
|
]]></string>
|
||||||
<string name="dark_mode">Dark Mode</string>
|
<string name="dark_mode">Dark Mode</string>
|
||||||
|
|
||||||
<string name="dismiss">Dismiss</string>
|
<string name="dismiss">Dismiss</string>
|
||||||
<string name="add_site">Add Site</string>
|
<string name="add_site">Add Site</string>
|
||||||
<string name="site_name">Site Name</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">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_name">Please enter a name!</string>
|
||||||
<string name="please_enter_url">Please enter a URL.</string>
|
<string name="please_enter_url">Please enter a URL.</string>
|
||||||
<string name="please_enter_valid_url">Please enter a valid URL.</string>
|
<string name="please_enter_valid_url">Please enter a valid URL.</string>
|
||||||
<string name="please_enter_check_interval">Please input a validation interval.</string>
|
|
||||||
<string name="please_enter_search_term">Please input a search term.</string>
|
<string name="please_enter_search_term">Please input a search term.</string>
|
||||||
<string name="please_enter_javaScript">Please input a validation script.</string>
|
|
||||||
<string name="please_enter_networkTimeout">Please enter a network timeout greater than 0.</string>
|
<string name="please_enter_networkTimeout">Please enter a network timeout greater than 0.</string>
|
||||||
|
<string name="please_enter_validCertUri">Certificate should be a valid file or content URI.</string>
|
||||||
|
|
||||||
<string name="options">Options</string>
|
<string name="options">Options</string>
|
||||||
<string name="remove_site">Remove Site</string>
|
<string name="remove_site">Remove Site</string>
|
||||||
|
<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_site_prompt"><![CDATA[Remove <b>%1$s</b> from your sites?]]></string>
|
||||||
<string name="remove">Remove</string>
|
<string name="remove">Remove</string>
|
||||||
<string name="save_changes">Save Changes</string>
|
<string name="save_changes">Save Changes</string>
|
||||||
|
@ -43,8 +50,8 @@
|
||||||
<string name="disable_automatic_checks">Disable Automatic Validation</string>
|
<string name="disable_automatic_checks">Disable Automatic Validation</string>
|
||||||
<string name="disable_automatic_checks_prompt"><![CDATA[
|
<string name="disable_automatic_checks_prompt"><![CDATA[
|
||||||
Disable automatic validation for <b>%1$s</b>? The site will not be checked in the background
|
Disable automatic validation for <b>%1$s</b>? The site will not be checked in the background
|
||||||
until you re-enable validation for it. You can still manually perform validation by tapping the
|
until you re-enable validation by tapping the checkmark (Save) icon. You can still manually
|
||||||
Refresh icon at the top of this page.
|
perform validation by tapping the Refresh icon at the top of this page.
|
||||||
]]></string>
|
]]></string>
|
||||||
<string name="disable">Disable</string>
|
<string name="disable">Disable</string>
|
||||||
<string name="renable_and_save_changes">Enable Auto Validation & Save Changes</string>
|
<string name="renable_and_save_changes">Enable Auto Validation & Save Changes</string>
|
||||||
|
@ -52,6 +59,10 @@
|
||||||
<string name="response_timeout">Network Response Timeout (ms)</string>
|
<string name="response_timeout">Network Response Timeout (ms)</string>
|
||||||
<string name="response_timeout_default">10000</string>
|
<string name="response_timeout_default">10000</string>
|
||||||
|
|
||||||
|
<string name="ssl_certificate">SSL Certificate</string>
|
||||||
|
<string name="ssl_certificate_automatic">(Automatic)</string>
|
||||||
|
<string name="ssl_certificate_browse">Browse</string>
|
||||||
|
|
||||||
<string name="refresh_status">Refresh Status</string>
|
<string name="refresh_status">Refresh Status</string>
|
||||||
|
|
||||||
<string name="warning_http_url">
|
<string name="warning_http_url">
|
||||||
|
@ -74,14 +85,6 @@
|
||||||
exception to pass custom error messages to Nock Nock.
|
exception to pass custom error messages to Nock Nock.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="support_me">Donate</string>
|
|
||||||
<string name="support_me_message"><![CDATA[
|
|
||||||
<b>Nock Nock</b> was created and is maintained by one person. Donations are <b>much</b>
|
|
||||||
appreciated and encourage continued support.
|
|
||||||
]]></string>
|
|
||||||
<string name="thank_you">Thank you very much!</string>
|
|
||||||
<string name="next">Next</string>
|
|
||||||
|
|
||||||
<string name="install_web_browser">Please install a web browser app, such as Google Chrome.</string>
|
<string name="install_web_browser">Please install a web browser app, such as Google Chrome.</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -4,15 +4,9 @@
|
||||||
|
|
||||||
<style name="AppTheme.Dark" parent="AppThemeParent.Dark"/>
|
<style name="AppTheme.Dark" parent="AppThemeParent.Dark"/>
|
||||||
|
|
||||||
<style name="AccentButton" parent="Widget.MaterialComponents.Button">
|
|
||||||
<item name="android:textColor">#fff</item>
|
|
||||||
<item name="backgroundTint">@color/colorAccent</item>
|
|
||||||
<item name="android:fontFamily">@font/lato</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="PrimaryDarkButton" parent="Widget.MaterialComponents.Button">
|
<style name="PrimaryDarkButton" parent="Widget.MaterialComponents.Button">
|
||||||
<item name="android:textColor">#fff</item>
|
<item name="android:textColor">#fff</item>
|
||||||
<item name="backgroundTint">@color/lighterGray</item>
|
<item name="backgroundTint">@color/darkerGray</item>
|
||||||
<item name="android:fontFamily">@font/lato</item>
|
<item name="android:fontFamily">@font/lato</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
<item name="toolbarTitleColor">#000000</item>
|
<item name="toolbarTitleColor">#000000</item>
|
||||||
<item name="dividerColor">#EEEEEE</item>
|
<item name="dividerColor">#EEEEEE</item>
|
||||||
<item name="iconColor">#000000</item>
|
<item name="iconColor">#000000</item>
|
||||||
|
<item name="scriptLayoutBackground">@color/lighterGray</item>
|
||||||
|
|
||||||
<item name="android:textColorPrimary">#212121</item>
|
<item name="android:textColorPrimary">#212121</item>
|
||||||
<item name="android:textColorSecondary">#727272</item>
|
<item name="android:textColorSecondary">#727272</item>
|
||||||
|
@ -33,6 +34,7 @@
|
||||||
<item name="toolbarTitleColor">#ffffff</item>
|
<item name="toolbarTitleColor">#ffffff</item>
|
||||||
<item name="dividerColor">#303030</item>
|
<item name="dividerColor">#303030</item>
|
||||||
<item name="iconColor">#FFFFFF</item>
|
<item name="iconColor">#FFFFFF</item>
|
||||||
|
<item name="scriptLayoutBackground">@color/darkerGray</item>
|
||||||
|
|
||||||
<item name="android:textColorPrimary">#FFFFFF</item>
|
<item name="android:textColorPrimary">#FFFFFF</item>
|
||||||
<item name="android:textColorSecondary">#F0F0F0</item>
|
<item name="android:textColorSecondary">#F0F0F0</item>
|
||||||
|
|
|
@ -6,4 +6,28 @@
|
||||||
<item name="android:textColor">?toolbarTitleColor</item>
|
<item name="android:textColor">?toolbarTitleColor</item>
|
||||||
</style>
|
</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>
|
</resources>
|
||||||
|
|
|
@ -19,11 +19,13 @@ import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import com.afollestad.nocknock.data.AppDatabase
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.data.RetryPolicy
|
import com.afollestad.nocknock.data.HeaderDao
|
||||||
import com.afollestad.nocknock.data.RetryPolicyDao
|
import com.afollestad.nocknock.data.RetryPolicyDao
|
||||||
import com.afollestad.nocknock.data.SiteDao
|
import com.afollestad.nocknock.data.SiteDao
|
||||||
import com.afollestad.nocknock.data.SiteSettingsDao
|
import com.afollestad.nocknock.data.SiteSettingsDao
|
||||||
import com.afollestad.nocknock.data.ValidationResultsDao
|
import com.afollestad.nocknock.data.ValidationResultsDao
|
||||||
|
import com.afollestad.nocknock.data.model.Header
|
||||||
|
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.SiteSettings
|
import com.afollestad.nocknock.data.model.SiteSettings
|
||||||
import com.afollestad.nocknock.data.model.Status
|
import com.afollestad.nocknock.data.model.Status
|
||||||
|
@ -55,7 +57,8 @@ fun fakeSettingsModel(
|
||||||
validationMode = validationMode,
|
validationMode = validationMode,
|
||||||
validationArgs = null,
|
validationArgs = null,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
networkTimeout = 10000
|
networkTimeout = 10000,
|
||||||
|
certificate = null
|
||||||
)
|
)
|
||||||
|
|
||||||
fun fakeResultModel(
|
fun fakeResultModel(
|
||||||
|
@ -79,18 +82,31 @@ fun fakeRetryPolicy(
|
||||||
minutes = minutes
|
minutes = minutes
|
||||||
)
|
)
|
||||||
|
|
||||||
fun fakeModel(id: Long) = Site(
|
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(
|
||||||
id = id,
|
id = id,
|
||||||
name = "Test",
|
name = "Test",
|
||||||
url = "https://test.com",
|
url = "https://test.com",
|
||||||
|
tags = tags,
|
||||||
settings = fakeSettingsModel(id),
|
settings = fakeSettingsModel(id),
|
||||||
lastResult = fakeResultModel(id),
|
lastResult = fakeResultModel(id),
|
||||||
retryPolicy = fakeRetryPolicy(id)
|
retryPolicy = fakeRetryPolicy(id),
|
||||||
|
headers = fakeHeaders(id)
|
||||||
)
|
)
|
||||||
|
|
||||||
val MOCK_MODEL_1 = fakeModel(1)
|
val MOCK_MODEL_1 = fakeModel(1, tags = "one,two")
|
||||||
val MOCK_MODEL_2 = fakeModel(2)
|
val MOCK_MODEL_2 = fakeModel(2, tags = "three,four")
|
||||||
val MOCK_MODEL_3 = fakeModel(3)
|
val MOCK_MODEL_3 = fakeModel(3, tags = "five,six")
|
||||||
|
|
||||||
val ALL_MOCK_MODELS = listOf(MOCK_MODEL_1, MOCK_MODEL_2, MOCK_MODEL_3)
|
val ALL_MOCK_MODELS = listOf(MOCK_MODEL_1, MOCK_MODEL_2, MOCK_MODEL_3)
|
||||||
|
|
||||||
fun mockDatabase(): AppDatabase {
|
fun mockDatabase(): AppDatabase {
|
||||||
|
@ -155,12 +171,29 @@ fun mockDatabase(): AppDatabase {
|
||||||
on { update(isA()) } doReturn 1
|
on { update(isA()) } doReturn 1
|
||||||
on { delete(isA()) } doReturn 1
|
on { delete(isA()) } doReturn 1
|
||||||
}
|
}
|
||||||
|
val headerDao = mock<HeaderDao> {
|
||||||
|
on { all() } doReturn MOCK_MODEL_1.headers + MOCK_MODEL_2.headers + MOCK_MODEL_3.headers
|
||||||
|
on { forSite(isA()) } doAnswer { inv ->
|
||||||
|
val id = inv.getArgument<Long>(0)
|
||||||
|
return@doAnswer when (id) {
|
||||||
|
1L -> MOCK_MODEL_1.headers
|
||||||
|
2L -> MOCK_MODEL_2.headers
|
||||||
|
3L -> MOCK_MODEL_3.headers
|
||||||
|
else -> listOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
on { insert(isA<Header>()) } doReturn 1L
|
||||||
|
on { insert(isA<List<Header>>()) } doReturn listOf(1L, 2L)
|
||||||
|
on { update(isA()) } doReturn 1
|
||||||
|
on { delete(isA()) } doReturn 1
|
||||||
|
}
|
||||||
|
|
||||||
return mock {
|
return mock {
|
||||||
on { siteDao() } doReturn siteDao
|
on { siteDao() } doReturn siteDao
|
||||||
on { siteSettingsDao() } doReturn settingsDao
|
on { siteSettingsDao() } doReturn settingsDao
|
||||||
on { validationResultsDao() } doReturn resultsDao
|
on { validationResultsDao() } doReturn resultsDao
|
||||||
on { retryPolicyDao() } doReturn retryDao
|
on { retryPolicyDao() } doReturn retryDao
|
||||||
|
on { headerDao() } doReturn headerDao
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,20 +17,21 @@ package com.afollestad.nocknock.ui.addsite
|
||||||
|
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
|
import com.afollestad.nocknock.data.model.Header
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.SiteSettings
|
import com.afollestad.nocknock.data.model.SiteSettings
|
||||||
|
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||||
import com.afollestad.nocknock.engine.validation.ValidationManager
|
import com.afollestad.nocknock.data.model.ValidationResult
|
||||||
|
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||||
import com.afollestad.nocknock.mockDatabase
|
import com.afollestad.nocknock.mockDatabase
|
||||||
import com.afollestad.nocknock.utilities.ext.MINUTE
|
import com.afollestad.nocknock.utilities.ext.MINUTE
|
||||||
import com.afollestad.nocknock.utilities.livedata.test
|
import com.afollestad.nocknock.utilities.livedata.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import com.nhaarman.mockitokotlin2.any
|
|
||||||
import com.nhaarman.mockitokotlin2.argumentCaptor
|
import com.nhaarman.mockitokotlin2.argumentCaptor
|
||||||
import com.nhaarman.mockitokotlin2.mock
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
import com.nhaarman.mockitokotlin2.never
|
|
||||||
import com.nhaarman.mockitokotlin2.verify
|
import com.nhaarman.mockitokotlin2.verify
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
@ -44,7 +45,7 @@ import org.junit.Test
|
||||||
class AddSiteViewModelTest {
|
class AddSiteViewModelTest {
|
||||||
|
|
||||||
private val database = mockDatabase()
|
private val database = mockDatabase()
|
||||||
private val validationManager = mock<ValidationManager>()
|
private val validationManager = mock<ValidationExecutor>()
|
||||||
|
|
||||||
@Rule @JvmField val rule = InstantTaskExecutorRule()
|
@Rule @JvmField val rule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
@ -149,247 +150,9 @@ class AddSiteViewModelTest {
|
||||||
assertThat(viewModel.getValidationArgs()).isEqualTo("Two")
|
assertThat(viewModel.getValidationArgs()).isEqualTo("Two")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun commit_nameError() {
|
|
||||||
val onNameError = viewModel.onNameError()
|
|
||||||
.test()
|
|
||||||
val onUrlError = viewModel.onUrlError()
|
|
||||||
.test()
|
|
||||||
val onTimeoutError = viewModel.onTimeoutError()
|
|
||||||
.test()
|
|
||||||
val onCheckIntervalError = viewModel.onCheckIntervalError()
|
|
||||||
.test()
|
|
||||||
val onSearchTermError = viewModel.onValidationSearchTermError()
|
|
||||||
.test()
|
|
||||||
val onScriptError = viewModel.onValidationScriptError()
|
|
||||||
.test()
|
|
||||||
|
|
||||||
fillInModel().apply {
|
|
||||||
name.value = ""
|
|
||||||
}
|
|
||||||
val onDone = mock<() -> Unit>()
|
|
||||||
viewModel.commit(onDone)
|
|
||||||
|
|
||||||
verify(validationManager, never())
|
|
||||||
.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 {
|
@Test fun commit_success() = runBlocking {
|
||||||
val isLoading = viewModel.onIsLoading()
|
val isLoading = viewModel.onIsLoading()
|
||||||
.test()
|
.test()
|
||||||
val onNameError = viewModel.onNameError()
|
|
||||||
.test()
|
|
||||||
val onUrlError = viewModel.onUrlError()
|
|
||||||
.test()
|
|
||||||
val onTimeoutError = viewModel.onTimeoutError()
|
|
||||||
.test()
|
|
||||||
val onSearchTermError = viewModel.onValidationSearchTermError()
|
|
||||||
.test()
|
|
||||||
val onScriptError = viewModel.onValidationScriptError()
|
|
||||||
.test()
|
|
||||||
val onCheckIntervalError = viewModel.onCheckIntervalError()
|
|
||||||
.test()
|
|
||||||
|
|
||||||
fillInModel()
|
fillInModel()
|
||||||
val onDone = mock<() -> Unit>()
|
val onDone = mock<() -> Unit>()
|
||||||
|
@ -397,31 +160,30 @@ class AddSiteViewModelTest {
|
||||||
|
|
||||||
val siteCaptor = argumentCaptor<Site>()
|
val siteCaptor = argumentCaptor<Site>()
|
||||||
val settingsCaptor = argumentCaptor<SiteSettings>()
|
val settingsCaptor = argumentCaptor<SiteSettings>()
|
||||||
|
val validationResultCaptor = argumentCaptor<ValidationResult>()
|
||||||
|
|
||||||
isLoading.assertValues(true, false)
|
isLoading.assertValues(true, false)
|
||||||
verify(database.siteDao()).insert(siteCaptor.capture())
|
verify(database.siteDao()).insert(siteCaptor.capture())
|
||||||
verify(database.siteSettingsDao()).insert(settingsCaptor.capture())
|
verify(database.siteSettingsDao()).insert(settingsCaptor.capture())
|
||||||
verify(database.validationResultsDao(), never()).insert(any())
|
verify(database.validationResultsDao()).insert(validationResultCaptor.capture())
|
||||||
|
|
||||||
val settings = settingsCaptor.firstValue
|
val settings = settingsCaptor.firstValue
|
||||||
|
val result = validationResultCaptor.firstValue.copy(siteId = 1)
|
||||||
val model = siteCaptor.firstValue.copy(
|
val model = siteCaptor.firstValue.copy(
|
||||||
id = 1, // fill it in because our insert captor doesn't catch this
|
id = 1, // fill it in because our insert captor doesn't catch this
|
||||||
settings = settings,
|
settings = settings,
|
||||||
lastResult = null
|
lastResult = result
|
||||||
)
|
)
|
||||||
|
|
||||||
verify(validationManager).scheduleCheck(
|
assertThat(result.reason).isNull()
|
||||||
|
assertThat(result.status).isEqualTo(WAITING)
|
||||||
|
|
||||||
|
verify(validationManager).scheduleValidation(
|
||||||
site = model,
|
site = model,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true,
|
cancelPrevious = true,
|
||||||
fromFinishingJob = false
|
fromFinishingJob = false
|
||||||
)
|
)
|
||||||
onNameError.assertNoValues()
|
|
||||||
onUrlError.assertNoValues()
|
|
||||||
onTimeoutError.assertNoValues()
|
|
||||||
onCheckIntervalError.assertNoValues()
|
|
||||||
onSearchTermError.assertNoValues()
|
|
||||||
onScriptError.assertNoValues()
|
|
||||||
|
|
||||||
verify(onDone).invoke()
|
verify(onDone).invoke()
|
||||||
}
|
}
|
||||||
|
@ -435,5 +197,10 @@ class AddSiteViewModelTest {
|
||||||
validationScript.value = null
|
validationScript.value = null
|
||||||
checkIntervalValue.value = 60
|
checkIntervalValue.value = 60
|
||||||
checkIntervalUnit.value = 1000
|
checkIntervalUnit.value = 1000
|
||||||
|
tags.value = "one,two"
|
||||||
|
headers.value = listOf(
|
||||||
|
Header(2L, 1L, key = "Content-Type", value = "text/html"),
|
||||||
|
Header(3L, 1L, key = "User-Agent", value = "NockNock")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ import com.afollestad.nocknock.ALL_MOCK_MODELS
|
||||||
import com.afollestad.nocknock.MOCK_MODEL_1
|
import com.afollestad.nocknock.MOCK_MODEL_1
|
||||||
import com.afollestad.nocknock.MOCK_MODEL_2
|
import com.afollestad.nocknock.MOCK_MODEL_2
|
||||||
import com.afollestad.nocknock.MOCK_MODEL_3
|
import com.afollestad.nocknock.MOCK_MODEL_3
|
||||||
import com.afollestad.nocknock.engine.validation.ValidationManager
|
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||||
import com.afollestad.nocknock.mockDatabase
|
import com.afollestad.nocknock.mockDatabase
|
||||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.utilities.livedata.test
|
import com.afollestad.nocknock.utilities.livedata.test
|
||||||
|
@ -39,7 +39,7 @@ class MainViewModelTest {
|
||||||
|
|
||||||
private val database = mockDatabase()
|
private val database = mockDatabase()
|
||||||
private val notificationManager = mock<NockNotificationManager>()
|
private val notificationManager = mock<NockNotificationManager>()
|
||||||
private val validationManager = mock<ValidationManager>()
|
private val validationManager = mock<ValidationExecutor>()
|
||||||
|
|
||||||
@Rule @JvmField val rule = InstantTaskExecutorRule()
|
@Rule @JvmField val rule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
@ -60,18 +60,45 @@ class MainViewModelTest {
|
||||||
.test()
|
.test()
|
||||||
val sites = viewModel.onSites()
|
val sites = viewModel.onSites()
|
||||||
.test()
|
.test()
|
||||||
|
val tags = viewModel.onTags()
|
||||||
|
.test()
|
||||||
|
val tagsVisibility = viewModel.onTagsListVisibility()
|
||||||
|
.test()
|
||||||
|
|
||||||
viewModel.onResume()
|
viewModel.onResume()
|
||||||
|
|
||||||
verify(notificationManager).cancelStatusNotifications()
|
verify(notificationManager).cancelStatusNotifications()
|
||||||
verify(validationManager).ensureScheduledChecks()
|
verify(validationManager).ensureScheduledValidations()
|
||||||
|
|
||||||
sites.assertValues(
|
sites.assertValues(ALL_MOCK_MODELS)
|
||||||
listOf(),
|
|
||||||
ALL_MOCK_MODELS
|
|
||||||
)
|
|
||||||
isLoading.assertValues(true, false)
|
isLoading.assertValues(true, false)
|
||||||
emptyTextVisibility.assertValues(false, false)
|
emptyTextVisibility.assertValues(false, false)
|
||||||
|
tags.assertValues(listOf("one", "two", "three", "four", "five", "six").sorted())
|
||||||
|
tagsVisibility.assertValues(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun onTagSelection() = runBlocking {
|
||||||
|
val isLoading = viewModel.onIsLoading()
|
||||||
|
.test()
|
||||||
|
val emptyTextVisibility = viewModel.onEmptyTextVisibility()
|
||||||
|
.test()
|
||||||
|
val sites = viewModel.onSites()
|
||||||
|
.test()
|
||||||
|
val tags = viewModel.onTags()
|
||||||
|
.test()
|
||||||
|
val tagsVisibility = viewModel.onTagsListVisibility()
|
||||||
|
.test()
|
||||||
|
|
||||||
|
viewModel.onTagSelection(listOf("four", "six"))
|
||||||
|
|
||||||
|
verify(notificationManager).cancelStatusNotifications()
|
||||||
|
verify(validationManager).ensureScheduledValidations()
|
||||||
|
|
||||||
|
sites.assertValues(listOf(MOCK_MODEL_2, MOCK_MODEL_3))
|
||||||
|
isLoading.assertValues(true, false)
|
||||||
|
emptyTextVisibility.assertValues(false, false)
|
||||||
|
tags.assertValues(listOf("one", "two", "three", "four", "five", "six").sorted())
|
||||||
|
tagsVisibility.assertValues(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun postSiteUpdate_notFound() {
|
@Test fun postSiteUpdate_notFound() {
|
||||||
|
@ -86,10 +113,7 @@ class MainViewModelTest {
|
||||||
.test()
|
.test()
|
||||||
|
|
||||||
viewModel.onResume()
|
viewModel.onResume()
|
||||||
sites.assertValues(
|
sites.assertValues(ALL_MOCK_MODELS)
|
||||||
listOf(),
|
|
||||||
ALL_MOCK_MODELS
|
|
||||||
)
|
|
||||||
|
|
||||||
val updatedModel2 = MOCK_MODEL_2.copy(
|
val updatedModel2 = MOCK_MODEL_2.copy(
|
||||||
name = "Wakanda Forever!!!"
|
name = "Wakanda Forever!!!"
|
||||||
|
@ -106,7 +130,7 @@ class MainViewModelTest {
|
||||||
@Test fun refreshSite() {
|
@Test fun refreshSite() {
|
||||||
viewModel.refreshSite(MOCK_MODEL_3)
|
viewModel.refreshSite(MOCK_MODEL_3)
|
||||||
|
|
||||||
verify(validationManager).scheduleCheck(
|
verify(validationManager).scheduleValidation(
|
||||||
site = MOCK_MODEL_3,
|
site = MOCK_MODEL_3,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true
|
cancelPrevious = true
|
||||||
|
@ -120,10 +144,7 @@ class MainViewModelTest {
|
||||||
.test()
|
.test()
|
||||||
|
|
||||||
viewModel.onResume()
|
viewModel.onResume()
|
||||||
sites.assertValues(
|
sites.assertValues(ALL_MOCK_MODELS)
|
||||||
listOf(),
|
|
||||||
ALL_MOCK_MODELS
|
|
||||||
)
|
|
||||||
isLoading.assertValues(true, false)
|
isLoading.assertValues(true, false)
|
||||||
|
|
||||||
val modifiedModel = MOCK_MODEL_1.copy(id = 11111)
|
val modifiedModel = MOCK_MODEL_1.copy(id = 11111)
|
||||||
|
@ -132,7 +153,7 @@ class MainViewModelTest {
|
||||||
sites.assertNoValues()
|
sites.assertNoValues()
|
||||||
isLoading.assertValues(true, false)
|
isLoading.assertValues(true, false)
|
||||||
|
|
||||||
verify(validationManager).cancelCheck(modifiedModel)
|
verify(validationManager).cancelScheduledValidation(modifiedModel)
|
||||||
verify(notificationManager).cancelStatusNotification(modifiedModel)
|
verify(notificationManager).cancelStatusNotification(modifiedModel)
|
||||||
verify(database.siteDao()).delete(modifiedModel)
|
verify(database.siteDao()).delete(modifiedModel)
|
||||||
verify(database.siteSettingsDao()).delete(modifiedModel.settings!!)
|
verify(database.siteSettingsDao()).delete(modifiedModel.settings!!)
|
||||||
|
@ -147,10 +168,7 @@ class MainViewModelTest {
|
||||||
.test()
|
.test()
|
||||||
|
|
||||||
viewModel.onResume()
|
viewModel.onResume()
|
||||||
sites.assertValues(
|
sites.assertValues(ALL_MOCK_MODELS)
|
||||||
listOf(),
|
|
||||||
ALL_MOCK_MODELS
|
|
||||||
)
|
|
||||||
isLoading.assertValues(true, false)
|
isLoading.assertValues(true, false)
|
||||||
|
|
||||||
val modelsWithout1 = ALL_MOCK_MODELS.toMutableList()
|
val modelsWithout1 = ALL_MOCK_MODELS.toMutableList()
|
||||||
|
@ -163,7 +181,7 @@ class MainViewModelTest {
|
||||||
isLoading.assertValues(true, false)
|
isLoading.assertValues(true, false)
|
||||||
emptyTextVisibility.assertValues(false, false, false)
|
emptyTextVisibility.assertValues(false, false, false)
|
||||||
|
|
||||||
verify(validationManager).cancelCheck(MOCK_MODEL_1)
|
verify(validationManager).cancelScheduledValidation(MOCK_MODEL_1)
|
||||||
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
|
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
|
||||||
verify(database.siteDao()).delete(MOCK_MODEL_1)
|
verify(database.siteDao()).delete(MOCK_MODEL_1)
|
||||||
verify(database.siteSettingsDao()).delete(MOCK_MODEL_1.settings!!)
|
verify(database.siteSettingsDao()).delete(MOCK_MODEL_1.settings!!)
|
||||||
|
|
|
@ -18,6 +18,8 @@ package com.afollestad.nocknock.ui.viewsite
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
import com.afollestad.nocknock.MOCK_MODEL_1
|
import com.afollestad.nocknock.MOCK_MODEL_1
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
|
import com.afollestad.nocknock.data.model.Header
|
||||||
|
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.SiteSettings
|
import com.afollestad.nocknock.data.model.SiteSettings
|
||||||
import com.afollestad.nocknock.data.model.Status.CHECKING
|
import com.afollestad.nocknock.data.model.Status.CHECKING
|
||||||
|
@ -28,7 +30,8 @@ import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||||
import com.afollestad.nocknock.data.model.ValidationResult
|
import com.afollestad.nocknock.data.model.ValidationResult
|
||||||
import com.afollestad.nocknock.engine.validation.ValidationManager
|
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||||
|
import com.afollestad.nocknock.fakeRetryPolicy
|
||||||
import com.afollestad.nocknock.mockDatabase
|
import com.afollestad.nocknock.mockDatabase
|
||||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.utilities.livedata.test
|
import com.afollestad.nocknock.utilities.livedata.test
|
||||||
|
@ -38,9 +41,10 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import com.nhaarman.mockitokotlin2.any
|
import com.nhaarman.mockitokotlin2.any
|
||||||
import com.nhaarman.mockitokotlin2.argumentCaptor
|
import com.nhaarman.mockitokotlin2.argumentCaptor
|
||||||
import com.nhaarman.mockitokotlin2.doAnswer
|
import com.nhaarman.mockitokotlin2.doAnswer
|
||||||
|
import com.nhaarman.mockitokotlin2.doReturn
|
||||||
import com.nhaarman.mockitokotlin2.mock
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
import com.nhaarman.mockitokotlin2.never
|
|
||||||
import com.nhaarman.mockitokotlin2.verify
|
import com.nhaarman.mockitokotlin2.verify
|
||||||
|
import com.nhaarman.mockitokotlin2.whenever
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
@ -75,7 +79,7 @@ class ViewSiteViewModelTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val database = mockDatabase()
|
private val database = mockDatabase()
|
||||||
private val validationManager = mock<ValidationManager>()
|
private val validationManager = mock<ValidationExecutor>()
|
||||||
private val notificationManager = mock<NockNotificationManager>()
|
private val notificationManager = mock<NockNotificationManager>()
|
||||||
|
|
||||||
@Rule @JvmField val rule = InstantTaskExecutorRule()
|
@Rule @JvmField val rule = InstantTaskExecutorRule()
|
||||||
|
@ -255,247 +259,11 @@ class ViewSiteViewModelTest {
|
||||||
.isEqualTo("Two")
|
.isEqualTo("Two")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun commit_nameError() {
|
|
||||||
val onNameError = viewModel.onNameError()
|
|
||||||
.test()
|
|
||||||
val onUrlError = viewModel.onUrlError()
|
|
||||||
.test()
|
|
||||||
val onTimeoutError = viewModel.onTimeoutError()
|
|
||||||
.test()
|
|
||||||
val onCheckIntervalError = viewModel.onCheckIntervalError()
|
|
||||||
.test()
|
|
||||||
val onSearchTermError = viewModel.onValidationSearchTermError()
|
|
||||||
.test()
|
|
||||||
val onScriptError = viewModel.onValidationScriptError()
|
|
||||||
.test()
|
|
||||||
|
|
||||||
fillInModel().apply {
|
|
||||||
name.value = ""
|
|
||||||
}
|
|
||||||
val onDone = mock<() -> Unit>()
|
|
||||||
viewModel.commit(onDone)
|
|
||||||
|
|
||||||
verify(validationManager, never())
|
|
||||||
.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 {
|
@Test fun commit_success() = runBlocking {
|
||||||
|
whenever(database.retryPolicyDao().forSite(any())).doReturn(listOf(fakeRetryPolicy(1)))
|
||||||
|
|
||||||
val isLoading = viewModel.onIsLoading()
|
val isLoading = viewModel.onIsLoading()
|
||||||
.test()
|
.test()
|
||||||
val onNameError = viewModel.onNameError()
|
|
||||||
.test()
|
|
||||||
val onUrlError = viewModel.onUrlError()
|
|
||||||
.test()
|
|
||||||
val onTimeoutError = viewModel.onTimeoutError()
|
|
||||||
.test()
|
|
||||||
val onSearchTermError = viewModel.onValidationSearchTermError()
|
|
||||||
.test()
|
|
||||||
val onScriptError = viewModel.onValidationScriptError()
|
|
||||||
.test()
|
|
||||||
val onCheckIntervalError = viewModel.onCheckIntervalError()
|
|
||||||
.test()
|
|
||||||
|
|
||||||
fillInModel()
|
fillInModel()
|
||||||
val onDone = mock<() -> Unit>()
|
val onDone = mock<() -> Unit>()
|
||||||
|
@ -506,11 +274,13 @@ class ViewSiteViewModelTest {
|
||||||
val siteCaptor = argumentCaptor<Site>()
|
val siteCaptor = argumentCaptor<Site>()
|
||||||
val settingsCaptor = argumentCaptor<SiteSettings>()
|
val settingsCaptor = argumentCaptor<SiteSettings>()
|
||||||
val resultCaptor = argumentCaptor<ValidationResult>()
|
val resultCaptor = argumentCaptor<ValidationResult>()
|
||||||
|
val retryPolicyCaptor = argumentCaptor<RetryPolicy>()
|
||||||
|
|
||||||
isLoading.assertValues(true, false)
|
isLoading.assertValues(true, false)
|
||||||
verify(database.siteDao()).update(siteCaptor.capture())
|
verify(database.siteDao()).update(siteCaptor.capture())
|
||||||
verify(database.siteSettingsDao()).update(settingsCaptor.capture())
|
verify(database.siteSettingsDao()).update(settingsCaptor.capture())
|
||||||
verify(database.validationResultsDao()).update(resultCaptor.capture())
|
verify(database.validationResultsDao()).update(resultCaptor.capture())
|
||||||
|
verify(database.retryPolicyDao()).update(retryPolicyCaptor.capture())
|
||||||
|
|
||||||
// From fillInModel() below
|
// From fillInModel() below
|
||||||
val updatedSettings = MOCK_MODEL_1.settings!!.copy(
|
val updatedSettings = MOCK_MODEL_1.settings!!.copy(
|
||||||
|
@ -523,31 +293,26 @@ class ViewSiteViewModelTest {
|
||||||
val updatedResult = MOCK_MODEL_1.lastResult!!.copy(
|
val updatedResult = MOCK_MODEL_1.lastResult!!.copy(
|
||||||
status = WAITING
|
status = WAITING
|
||||||
)
|
)
|
||||||
|
val retryPolicy = retryPolicyCaptor.firstValue
|
||||||
val updatedModel = MOCK_MODEL_1.copy(
|
val updatedModel = MOCK_MODEL_1.copy(
|
||||||
name = "Hello There",
|
name = "Hello There",
|
||||||
url = "https://www.hellothere.com",
|
url = "https://www.hellothere.com",
|
||||||
settings = updatedSettings,
|
settings = updatedSettings,
|
||||||
lastResult = updatedResult
|
lastResult = updatedResult,
|
||||||
|
retryPolicy = retryPolicy
|
||||||
)
|
)
|
||||||
|
|
||||||
assertThat(siteCaptor.firstValue).isEqualTo(updatedModel)
|
assertThat(siteCaptor.firstValue).isEqualTo(updatedModel)
|
||||||
assertThat(settingsCaptor.firstValue).isEqualTo(updatedSettings)
|
assertThat(settingsCaptor.firstValue).isEqualTo(updatedSettings)
|
||||||
assertThat(resultCaptor.firstValue).isEqualTo(updatedResult)
|
assertThat(resultCaptor.firstValue).isEqualTo(updatedResult)
|
||||||
|
|
||||||
verify(validationManager).scheduleCheck(
|
verify(validationManager).scheduleValidation(
|
||||||
site = updatedModel,
|
site = updatedModel,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true,
|
cancelPrevious = true,
|
||||||
fromFinishingJob = false
|
fromFinishingJob = false
|
||||||
)
|
)
|
||||||
|
|
||||||
onNameError.assertNoValues()
|
|
||||||
onUrlError.assertNoValues()
|
|
||||||
onTimeoutError.assertNoValues()
|
|
||||||
onCheckIntervalError.assertNoValues()
|
|
||||||
onSearchTermError.assertNoValues()
|
|
||||||
onScriptError.assertNoValues()
|
|
||||||
|
|
||||||
verify(onDone).invoke()
|
verify(onDone).invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -562,7 +327,7 @@ class ViewSiteViewModelTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
viewModel.checkNow()
|
viewModel.checkNow()
|
||||||
verify(validationManager).scheduleCheck(
|
verify(validationManager).scheduleValidation(
|
||||||
site = expectedModel,
|
site = expectedModel,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true
|
cancelPrevious = true
|
||||||
|
@ -579,7 +344,7 @@ class ViewSiteViewModelTest {
|
||||||
viewModel.removeSite(onDone)
|
viewModel.removeSite(onDone)
|
||||||
isLoading.assertValues(true, false)
|
isLoading.assertValues(true, false)
|
||||||
|
|
||||||
verify(validationManager).cancelCheck(MOCK_MODEL_1)
|
verify(validationManager).cancelScheduledValidation(MOCK_MODEL_1)
|
||||||
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
|
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
|
||||||
verify(database.siteDao()).delete(MOCK_MODEL_1)
|
verify(database.siteDao()).delete(MOCK_MODEL_1)
|
||||||
verify(database.siteSettingsDao()).delete(MOCK_MODEL_1.settings!!)
|
verify(database.siteSettingsDao()).delete(MOCK_MODEL_1.settings!!)
|
||||||
|
@ -603,7 +368,7 @@ class ViewSiteViewModelTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
verify(validationManager).cancelCheck(MOCK_MODEL_1)
|
verify(validationManager).cancelScheduledValidation(MOCK_MODEL_1)
|
||||||
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
|
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
|
||||||
verify(database.siteDao()).update(expectedSite)
|
verify(database.siteDao()).update(expectedSite)
|
||||||
verify(database.siteSettingsDao()).update(expectedSite.settings!!)
|
verify(database.siteSettingsDao()).update(expectedSite.settings!!)
|
||||||
|
@ -619,5 +384,12 @@ class ViewSiteViewModelTest {
|
||||||
validationScript.value = "throw 'Oh no!'"
|
validationScript.value = "throw 'Oh no!'"
|
||||||
checkIntervalValue.value = 24
|
checkIntervalValue.value = 24
|
||||||
checkIntervalUnit.value = 60000
|
checkIntervalUnit.value = 60000
|
||||||
|
tags.value = "one,two"
|
||||||
|
retryPolicyTimes.value = 5
|
||||||
|
retryPolicyMinutes.value = 5
|
||||||
|
headers.value = listOf(
|
||||||
|
Header(2L, 1L, key = "Content-Type", value = "text/html"),
|
||||||
|
Header(3L, 1L, key = "User-Agent", value = "NockNock")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
art/showcase5.png
Normal file
After Width: | Height: | Size: 528 KiB |
Before Width: | Height: | Size: 678 KiB |
|
@ -15,6 +15,7 @@ buildscript {
|
||||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:' + versions.kotlin
|
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:' + versions.kotlin
|
||||||
classpath 'com.github.ben-manes:gradle-versions-plugin:' + versions.versionPlugin
|
classpath 'com.github.ben-manes:gradle-versions-plugin:' + versions.versionPlugin
|
||||||
classpath 'io.fabric.tools:gradle:' + versions.fabricPlugin
|
classpath 'io.fabric.tools:gradle:' + versions.fabricPlugin
|
||||||
|
classpath 'com.google.gms:google-services:' + versions.googleServices
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +23,6 @@ allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
maven { url "https://dl.bintray.com/drummer-aidan/maven" }
|
|
||||||
maven { url "https://jitpack.io" }
|
maven { url "https://jitpack.io" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,10 @@ android {
|
||||||
versionName versions.publishVersion
|
versionName versions.publishVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
exclude 'META-INF/atomicfu.kotlin_module'
|
||||||
|
}
|
||||||
|
|
||||||
// For Mozilla Rhino
|
// For Mozilla Rhino
|
||||||
lintOptions {
|
lintOptions {
|
||||||
abortOnError false
|
abortOnError false
|
||||||
|
@ -30,6 +34,7 @@ dependencies {
|
||||||
implementation 'org.mozilla:rhino:' + versions.rhino
|
implementation 'org.mozilla:rhino:' + versions.rhino
|
||||||
|
|
||||||
api 'com.afollestad:rxkprefs:' + versions.rxkPrefs
|
api 'com.afollestad:rxkprefs:' + versions.rxkPrefs
|
||||||
|
api "io.reactivex.rxjava2:rxjava:" + versions.rxJava
|
||||||
|
|
||||||
testImplementation 'junit:junit:' + versions.junit
|
testImplementation 'junit:junit:' + versions.junit
|
||||||
testImplementation 'com.google.truth:truth:' + versions.truth
|
testImplementation 'com.google.truth:truth:' + versions.truth
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* 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,7 +21,12 @@ import android.widget.EditText
|
||||||
import androidx.annotation.IntRange
|
import androidx.annotation.IntRange
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
fun EditText.setTextAndMaintainSelection(text: CharSequence) {
|
fun EditText.setTextAndMaintainSelection(text: CharSequence?) {
|
||||||
|
if (text == null) {
|
||||||
|
setText("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val formerStart = min(selectionStart, text.length)
|
val formerStart = min(selectionStart, text.length)
|
||||||
val formerEnd = min(selectionEnd, text.length)
|
val formerEnd = min(selectionEnd, text.length)
|
||||||
setText(text)
|
setText(text)
|
||||||
|
|
|
@ -30,6 +30,8 @@ interface CanNotifyModel : Serializable {
|
||||||
fun notifyName(): String
|
fun notifyName(): String
|
||||||
|
|
||||||
fun notifyTag(): String
|
fun notifyTag(): String
|
||||||
|
|
||||||
|
fun notifyDescription(): String?
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationCompat.BigTextStyle
|
||||||
import androidx.core.app.NotificationCompat.DEFAULT_VIBRATE
|
import androidx.core.app.NotificationCompat.DEFAULT_VIBRATE
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
|
@ -56,6 +57,10 @@ class RealNotificationProvider(
|
||||||
.setLargeIcon(largeIcon)
|
.setLargeIcon(largeIcon)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setDefaults(DEFAULT_VIBRATE)
|
.setDefaults(DEFAULT_VIBRATE)
|
||||||
|
.setStyle(
|
||||||
|
BigTextStyle()
|
||||||
|
.bigText(content)
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,10 @@ android {
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
exclude 'META-INF/atomicfu.kotlin_module'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
<uses-library
|
<uses-library
|
||||||
android:name="android.test.runner"
|
android:name="androidx.test.runner"
|
||||||
android:required="false"/>
|
android:required="false"/>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -21,6 +21,8 @@ import android.content.Context
|
||||||
import androidx.room.Room.inMemoryDatabaseBuilder
|
import androidx.room.Room.inMemoryDatabaseBuilder
|
||||||
import androidx.test.core.app.ApplicationProvider.getApplicationContext
|
import androidx.test.core.app.ApplicationProvider.getApplicationContext
|
||||||
import androidx.test.runner.AndroidJUnit4
|
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.Site
|
||||||
import com.afollestad.nocknock.data.model.SiteSettings
|
import com.afollestad.nocknock.data.model.SiteSettings
|
||||||
import com.afollestad.nocknock.data.model.Status.ERROR
|
import com.afollestad.nocknock.data.model.Status.ERROR
|
||||||
|
@ -46,6 +48,7 @@ class AppDatabaseTest() {
|
||||||
private lateinit var settingsDao: SiteSettingsDao
|
private lateinit var settingsDao: SiteSettingsDao
|
||||||
private lateinit var resultsDao: ValidationResultsDao
|
private lateinit var resultsDao: ValidationResultsDao
|
||||||
private lateinit var retryDao: RetryPolicyDao
|
private lateinit var retryDao: RetryPolicyDao
|
||||||
|
private lateinit var headerDao: HeaderDao
|
||||||
|
|
||||||
@Before fun setup() {
|
@Before fun setup() {
|
||||||
val context = getApplicationContext<Context>()
|
val context = getApplicationContext<Context>()
|
||||||
|
@ -54,13 +57,12 @@ class AppDatabaseTest() {
|
||||||
settingsDao = db.siteSettingsDao()
|
settingsDao = db.siteSettingsDao()
|
||||||
resultsDao = db.validationResultsDao()
|
resultsDao = db.validationResultsDao()
|
||||||
retryDao = db.retryPolicyDao()
|
retryDao = db.retryPolicyDao()
|
||||||
|
headerDao = db.headerDao()
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun destroy() {
|
fun destroy() = db.close()
|
||||||
db.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SiteDao
|
// SiteDao
|
||||||
|
|
||||||
|
@ -68,9 +70,11 @@ class AppDatabaseTest() {
|
||||||
val model1 = Site(
|
val model1 = Site(
|
||||||
name = "Test 1",
|
name = "Test 1",
|
||||||
url = "https://test1.com",
|
url = "https://test1.com",
|
||||||
|
tags = "",
|
||||||
settings = null,
|
settings = null,
|
||||||
lastResult = null,
|
lastResult = null,
|
||||||
retryPolicy = null
|
retryPolicy = null,
|
||||||
|
headers = emptyList()
|
||||||
)
|
)
|
||||||
val newId1 = sitesDao.insert(model1)
|
val newId1 = sitesDao.insert(model1)
|
||||||
assertThat(newId1).isGreaterThan(0)
|
assertThat(newId1).isGreaterThan(0)
|
||||||
|
@ -78,9 +82,11 @@ class AppDatabaseTest() {
|
||||||
val model2 = Site(
|
val model2 = Site(
|
||||||
name = "Test 2",
|
name = "Test 2",
|
||||||
url = "https://test2.com",
|
url = "https://test2.com",
|
||||||
|
tags = "",
|
||||||
settings = null,
|
settings = null,
|
||||||
lastResult = null,
|
lastResult = null,
|
||||||
retryPolicy = null
|
retryPolicy = null,
|
||||||
|
headers = emptyList()
|
||||||
)
|
)
|
||||||
val newId2 = sitesDao.insert(model2)
|
val newId2 = sitesDao.insert(model2)
|
||||||
assertThat(newId2).isGreaterThan(newId1)
|
assertThat(newId2).isGreaterThan(newId1)
|
||||||
|
@ -95,9 +101,11 @@ class AppDatabaseTest() {
|
||||||
val model = Site(
|
val model = Site(
|
||||||
name = "Test",
|
name = "Test",
|
||||||
url = "https://test.com",
|
url = "https://test.com",
|
||||||
|
tags = "",
|
||||||
settings = null,
|
settings = null,
|
||||||
lastResult = null,
|
lastResult = null,
|
||||||
retryPolicy = null
|
retryPolicy = null,
|
||||||
|
headers = emptyList()
|
||||||
)
|
)
|
||||||
val newId = sitesDao.insert(model)
|
val newId = sitesDao.insert(model)
|
||||||
assertThat(newId).isGreaterThan(0)
|
assertThat(newId).isGreaterThan(0)
|
||||||
|
@ -110,9 +118,11 @@ class AppDatabaseTest() {
|
||||||
val initialModel = Site(
|
val initialModel = Site(
|
||||||
name = "Test 1",
|
name = "Test 1",
|
||||||
url = "https://test1.com",
|
url = "https://test1.com",
|
||||||
|
tags = "",
|
||||||
settings = null,
|
settings = null,
|
||||||
lastResult = null,
|
lastResult = null,
|
||||||
retryPolicy = null
|
retryPolicy = null,
|
||||||
|
headers = emptyList()
|
||||||
)
|
)
|
||||||
val newId = sitesDao.insert(initialModel)
|
val newId = sitesDao.insert(initialModel)
|
||||||
assertThat(newId).isGreaterThan(0)
|
assertThat(newId).isGreaterThan(0)
|
||||||
|
@ -134,9 +144,11 @@ class AppDatabaseTest() {
|
||||||
val model1 = Site(
|
val model1 = Site(
|
||||||
name = "Test 1",
|
name = "Test 1",
|
||||||
url = "https://test1.com",
|
url = "https://test1.com",
|
||||||
|
tags = "",
|
||||||
settings = null,
|
settings = null,
|
||||||
lastResult = null,
|
lastResult = null,
|
||||||
retryPolicy = null
|
retryPolicy = null,
|
||||||
|
headers = emptyList()
|
||||||
)
|
)
|
||||||
val newId1 = sitesDao.insert(model1)
|
val newId1 = sitesDao.insert(model1)
|
||||||
assertThat(newId1).isGreaterThan(0)
|
assertThat(newId1).isGreaterThan(0)
|
||||||
|
@ -144,9 +156,11 @@ class AppDatabaseTest() {
|
||||||
val model2 = Site(
|
val model2 = Site(
|
||||||
name = "Test 2",
|
name = "Test 2",
|
||||||
url = "https://test2.com",
|
url = "https://test2.com",
|
||||||
|
tags = "",
|
||||||
settings = null,
|
settings = null,
|
||||||
lastResult = null,
|
lastResult = null,
|
||||||
retryPolicy = null
|
retryPolicy = null,
|
||||||
|
headers = emptyList()
|
||||||
)
|
)
|
||||||
val newId2 = sitesDao.insert(model2)
|
val newId2 = sitesDao.insert(model2)
|
||||||
assertThat(newId2).isGreaterThan(newId1)
|
assertThat(newId2).isGreaterThan(newId1)
|
||||||
|
@ -167,7 +181,8 @@ class AppDatabaseTest() {
|
||||||
validationMode = STATUS_CODE,
|
validationMode = STATUS_CODE,
|
||||||
validationArgs = null,
|
validationArgs = null,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
networkTimeout = 10000
|
networkTimeout = 10000,
|
||||||
|
certificate = null
|
||||||
)
|
)
|
||||||
val newId = settingsDao.insert(model)
|
val newId = settingsDao.insert(model)
|
||||||
assertThat(newId).isEqualTo(1)
|
assertThat(newId).isEqualTo(1)
|
||||||
|
@ -185,7 +200,8 @@ class AppDatabaseTest() {
|
||||||
validationMode = STATUS_CODE,
|
validationMode = STATUS_CODE,
|
||||||
validationArgs = null,
|
validationArgs = null,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
networkTimeout = 10000
|
networkTimeout = 10000,
|
||||||
|
certificate = null
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -213,7 +229,8 @@ class AppDatabaseTest() {
|
||||||
validationMode = STATUS_CODE,
|
validationMode = STATUS_CODE,
|
||||||
validationArgs = null,
|
validationArgs = null,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
networkTimeout = 10000
|
networkTimeout = 10000,
|
||||||
|
certificate = null
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -291,7 +308,7 @@ class AppDatabaseTest() {
|
||||||
val newId = retryDao.insert(model)
|
val newId = retryDao.insert(model)
|
||||||
assertThat(newId).isEqualTo(1)
|
assertThat(newId).isEqualTo(1)
|
||||||
|
|
||||||
val finalModel = resultsDao.forSite(newId)
|
val finalModel = retryDao.forSite(newId)
|
||||||
.single()
|
.single()
|
||||||
assertThat(finalModel).isEqualTo(model.copy(siteId = newId))
|
assertThat(finalModel).isEqualTo(model.copy(siteId = newId))
|
||||||
}
|
}
|
||||||
|
@ -333,6 +350,78 @@ class AppDatabaseTest() {
|
||||||
assertThat(retryDao.forSite(1)).isEmpty()
|
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
|
// Extension Methods
|
||||||
|
|
||||||
@Test fun extension_put_and_allSites() {
|
@Test fun extension_put_and_allSites() {
|
||||||
|
@ -342,9 +431,30 @@ class AppDatabaseTest() {
|
||||||
|
|
||||||
val allSites = db.allSites()
|
val allSites = db.allSites()
|
||||||
assertThat(allSites.size).isEqualTo(3)
|
assertThat(allSites.size).isEqualTo(3)
|
||||||
assertThat(allSites[0]).isEqualTo(MOCK_MODEL_1)
|
assertThat(allSites[0]).isEqualTo(
|
||||||
assertThat(allSites[1]).isEqualTo(MOCK_MODEL_2)
|
MOCK_MODEL_1.copy(
|
||||||
assertThat(allSites[2]).isEqualTo(MOCK_MODEL_3)
|
headers = listOf(
|
||||||
|
MOCK_MODEL_1.headers.first().copy(id = 1),
|
||||||
|
MOCK_MODEL_1.headers.last().copy(id = 2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(allSites[1]).isEqualTo(
|
||||||
|
MOCK_MODEL_2.copy(
|
||||||
|
headers = listOf(
|
||||||
|
MOCK_MODEL_2.headers.first().copy(id = 3),
|
||||||
|
MOCK_MODEL_2.headers.last().copy(id = 4)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(allSites[2]).isEqualTo(
|
||||||
|
MOCK_MODEL_3.copy(
|
||||||
|
headers = listOf(
|
||||||
|
MOCK_MODEL_3.headers.first().copy(id = 5),
|
||||||
|
MOCK_MODEL_3.headers.last().copy(id = 6)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun extension_put_getSite() {
|
@Test fun extension_put_getSite() {
|
||||||
|
@ -379,12 +489,25 @@ class AppDatabaseTest() {
|
||||||
count = 4,
|
count = 4,
|
||||||
minutes = 8
|
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(
|
val updatedModel = modelToUpdate.copy(
|
||||||
name = "Oijrfouhef",
|
name = "Oijrfouhef",
|
||||||
url = "https://iojfdfsdk.io",
|
url = "https://iojfdfsdk.io",
|
||||||
settings = updatedSettings,
|
settings = updatedSettings,
|
||||||
lastResult = updatedValidationResult,
|
lastResult = updatedValidationResult,
|
||||||
retryPolicy = updatedRetryPolicy
|
retryPolicy = updatedRetryPolicy,
|
||||||
|
headers = updatedHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
db.updateSite(updatedModel)
|
db.updateSite(updatedModel)
|
||||||
|
@ -393,6 +516,8 @@ class AppDatabaseTest() {
|
||||||
assertThat(finalSite.settings).isEqualTo(updatedSettings)
|
assertThat(finalSite.settings).isEqualTo(updatedSettings)
|
||||||
assertThat(finalSite.lastResult).isEqualTo(updatedValidationResult)
|
assertThat(finalSite.lastResult).isEqualTo(updatedValidationResult)
|
||||||
assertThat(finalSite.retryPolicy).isEqualTo(updatedRetryPolicy)
|
assertThat(finalSite.retryPolicy).isEqualTo(updatedRetryPolicy)
|
||||||
|
assertThat(finalSite.headers.first()).isEqualTo(updatedHeaders.first())
|
||||||
|
assertThat(finalSite.headers.last()).isEqualTo(updatedHeaders.last())
|
||||||
assertThat(finalSite).isEqualTo(updatedModel)
|
assertThat(finalSite).isEqualTo(updatedModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -402,7 +527,7 @@ class AppDatabaseTest() {
|
||||||
db.putSite(MOCK_MODEL_3)
|
db.putSite(MOCK_MODEL_3)
|
||||||
val allSites = db.allSites()
|
val allSites = db.allSites()
|
||||||
|
|
||||||
db.deleteSite(MOCK_MODEL_2)
|
db.deleteSite(allSites[1])
|
||||||
|
|
||||||
val remainingSettings = settingsDao.all()
|
val remainingSettings = settingsDao.all()
|
||||||
assertThat(remainingSettings.size).isEqualTo(2)
|
assertThat(remainingSettings.size).isEqualTo(2)
|
||||||
|
@ -418,5 +543,12 @@ class AppDatabaseTest() {
|
||||||
assertThat(remainingRetryPolicies.size).isEqualTo(2)
|
assertThat(remainingRetryPolicies.size).isEqualTo(2)
|
||||||
assertThat(remainingRetryPolicies[0]).isEqualTo(allSites[0].retryPolicy!!)
|
assertThat(remainingRetryPolicies[0]).isEqualTo(allSites[0].retryPolicy!!)
|
||||||
assertThat(remainingRetryPolicies[1]).isEqualTo(allSites[2].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,6 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.data
|
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.Site
|
||||||
import com.afollestad.nocknock.data.model.SiteSettings
|
import com.afollestad.nocknock.data.model.SiteSettings
|
||||||
import com.afollestad.nocknock.data.model.Status
|
import com.afollestad.nocknock.data.model.Status
|
||||||
|
@ -33,7 +35,8 @@ fun fakeSettingsModel(
|
||||||
validationMode = validationMode,
|
validationMode = validationMode,
|
||||||
validationArgs = null,
|
validationArgs = null,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
networkTimeout = 10000
|
networkTimeout = 10000,
|
||||||
|
certificate = null
|
||||||
)
|
)
|
||||||
|
|
||||||
fun fakeResultModel(
|
fun fakeResultModel(
|
||||||
|
@ -57,13 +60,20 @@ fun fakeRetryPolicy(
|
||||||
minutes = minutes
|
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(
|
fun fakeModel(id: Long) = Site(
|
||||||
id = id,
|
id = id,
|
||||||
name = "Test",
|
name = "Test",
|
||||||
url = "https://test.com",
|
url = "https://test.com",
|
||||||
|
tags = "",
|
||||||
settings = fakeSettingsModel(id),
|
settings = fakeSettingsModel(id),
|
||||||
lastResult = fakeResultModel(id),
|
lastResult = fakeResultModel(id),
|
||||||
retryPolicy = fakeRetryPolicy(id)
|
retryPolicy = fakeRetryPolicy(id),
|
||||||
|
headers = fakeHeaders(id)
|
||||||
)
|
)
|
||||||
|
|
||||||
val MOCK_MODEL_1 = fakeModel(1)
|
val MOCK_MODEL_1 = fakeModel(1)
|
||||||
|
|
|
@ -19,6 +19,8 @@ import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import com.afollestad.nocknock.data.model.Converters
|
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.Site
|
||||||
import com.afollestad.nocknock.data.model.SiteSettings
|
import com.afollestad.nocknock.data.model.SiteSettings
|
||||||
import com.afollestad.nocknock.data.model.ValidationResult
|
import com.afollestad.nocknock.data.model.ValidationResult
|
||||||
|
@ -26,12 +28,13 @@ import com.afollestad.nocknock.data.model.ValidationResult
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
|
Header::class,
|
||||||
RetryPolicy::class,
|
RetryPolicy::class,
|
||||||
ValidationResult::class,
|
ValidationResult::class,
|
||||||
SiteSettings::class,
|
SiteSettings::class,
|
||||||
Site::class
|
Site::class
|
||||||
],
|
],
|
||||||
version = 2,
|
version = 5,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
@ -44,6 +47,8 @@ abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun validationResultsDao(): ValidationResultsDao
|
abstract fun validationResultsDao(): ValidationResultsDao
|
||||||
|
|
||||||
abstract fun retryPolicyDao(): RetryPolicyDao
|
abstract fun retryPolicyDao(): RetryPolicyDao
|
||||||
|
|
||||||
|
abstract fun headerDao(): HeaderDao
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,10 +65,12 @@ fun AppDatabase.allSites(): List<Site> {
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
val retryPolicy = retryPolicyDao().forSite(it.id)
|
val retryPolicy = retryPolicyDao().forSite(it.id)
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
|
val headers = headerDao().forSite(it.id)
|
||||||
return@map it.copy(
|
return@map it.copy(
|
||||||
settings = settings,
|
settings = settings,
|
||||||
lastResult = lastResult,
|
lastResult = lastResult,
|
||||||
retryPolicy = retryPolicy
|
retryPolicy = retryPolicy,
|
||||||
|
headers = headers
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,10 +89,12 @@ fun AppDatabase.getSite(id: Long): Site? {
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
val retryPolicy = retryPolicyDao().forSite(id)
|
val retryPolicy = retryPolicyDao().forSite(id)
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
|
val headers = headerDao().forSite(id)
|
||||||
return result.copy(
|
return result.copy(
|
||||||
settings = settings,
|
settings = settings,
|
||||||
lastResult = lastResult,
|
lastResult = lastResult,
|
||||||
retryPolicy = retryPolicy
|
retryPolicy = retryPolicy,
|
||||||
|
headers = headers
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,14 +109,19 @@ fun AppDatabase.putSite(site: Site): Site {
|
||||||
val settingsWithSiteId = settings.copy(siteId = newId)
|
val settingsWithSiteId = settings.copy(siteId = newId)
|
||||||
val lastResultWithSiteId = site.lastResult?.copy(siteId = newId)
|
val lastResultWithSiteId = site.lastResult?.copy(siteId = newId)
|
||||||
val retryPolicyWithSiteId = site.retryPolicy?.copy(siteId = newId)
|
val retryPolicyWithSiteId = site.retryPolicy?.copy(siteId = newId)
|
||||||
siteSettingsDao().insert(settingsWithSiteId)
|
val headersWithSiteId = site.headers.map { it.copy(siteId = newId) }
|
||||||
|
|
||||||
|
siteSettingsDao().insert(settingsWithSiteId)
|
||||||
lastResultWithSiteId?.let { validationResultsDao().insert(it) }
|
lastResultWithSiteId?.let { validationResultsDao().insert(it) }
|
||||||
retryPolicyWithSiteId?.let { retryPolicyDao().insert(it) }
|
retryPolicyWithSiteId?.let { retryPolicyDao().insert(it) }
|
||||||
|
headerDao().insert(headersWithSiteId)
|
||||||
|
|
||||||
return site.copy(
|
return site.copy(
|
||||||
id = newId,
|
id = newId,
|
||||||
settings = settingsWithSiteId
|
settings = settingsWithSiteId,
|
||||||
|
lastResult = lastResultWithSiteId,
|
||||||
|
retryPolicy = retryPolicyWithSiteId,
|
||||||
|
headers = headersWithSiteId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,6 +165,13 @@ fun AppDatabase.updateSite(site: Site) {
|
||||||
retryPolicyDao().insert(retryPolicy)
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -162,5 +183,9 @@ fun AppDatabase.deleteSite(site: Site) {
|
||||||
site.settings?.let { siteSettingsDao().delete(it) }
|
site.settings?.let { siteSettingsDao().delete(it) }
|
||||||
site.lastResult?.let { validationResultsDao().delete(it) }
|
site.lastResult?.let { validationResultsDao().delete(it) }
|
||||||
site.retryPolicy?.let { retryPolicyDao().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)
|
siteDao().delete(site)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,45 @@ class Database1to2Migration : Migration(1, 2) {
|
||||||
|
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
database.execSQL(
|
database.execSQL(
|
||||||
"CREATE TABLE `retry_policies` (siteId INTEGER PRIMARY KEY NOT NULL, count INTEGER NOT NULL, minutes INTEGER NOT NULL)"
|
"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)"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
47
data/src/main/java/com/afollestad/nocknock/data/HeaderDao.kt
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy.FAIL
|
import androidx.room.OnConflictStrategy.FAIL
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
|
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
@Dao
|
@Dao
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
* 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, "", "")
|
||||||
|
}
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
@file:Suppress("unused")
|
@file:Suppress("unused")
|
||||||
|
|
||||||
package com.afollestad.nocknock.data
|
package com.afollestad.nocknock.data.model
|
||||||
|
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
@ -57,6 +57,14 @@ data class RetryPolicy(
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
val timesPerMinute = count.toFloat() / minutes.toFloat()
|
val timesPerMinute = count.toFloat() / minutes.toFloat()
|
||||||
return MINUTE / timesPerMinute.toInt()
|
return MINUTE / timesPerMinute.toSafeInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Float.toSafeInt(): Int {
|
||||||
|
val intValue = toInt()
|
||||||
|
if (intValue == 0) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return intValue
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -18,7 +18,6 @@ package com.afollestad.nocknock.data.model
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Ignore
|
import androidx.room.Ignore
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.afollestad.nocknock.data.RetryPolicy
|
|
||||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||||
import com.afollestad.nocknock.utilities.ext.timeString
|
import com.afollestad.nocknock.utilities.ext.timeString
|
||||||
import com.afollestad.nocknock.utilities.providers.CanNotifyModel
|
import com.afollestad.nocknock.utilities.providers.CanNotifyModel
|
||||||
|
@ -32,17 +31,21 @@ data class Site(
|
||||||
@PrimaryKey(autoGenerate = true) var id: Long = 0,
|
@PrimaryKey(autoGenerate = true) var id: Long = 0,
|
||||||
/** The site's user-given name. */
|
/** The site's user-given name. */
|
||||||
var name: String,
|
var name: String,
|
||||||
/** The URl at which validation attempts are made to. */
|
/** The URL at which validation attempts are made to. */
|
||||||
var url: String,
|
var url: String,
|
||||||
|
/** Comma separated tags for this site. */
|
||||||
|
var tags: String,
|
||||||
/** Settings for the site. */
|
/** Settings for the site. */
|
||||||
@Ignore var settings: SiteSettings?,
|
@Ignore var settings: SiteSettings?,
|
||||||
/** The last validation attempt result for the site, if any. */
|
/** The last validation attempt result for the site, if any. */
|
||||||
@Ignore var lastResult: ValidationResult?,
|
@Ignore var lastResult: ValidationResult?,
|
||||||
/** The site's retry policy, if any. */
|
/** The site's retry policy, if any. */
|
||||||
@Ignore var retryPolicy: RetryPolicy?
|
@Ignore var retryPolicy: RetryPolicy?,
|
||||||
|
/** Request headers sent with this site's validation attempts. */
|
||||||
|
@Ignore var headers: List<Header>
|
||||||
) : CanNotifyModel {
|
) : CanNotifyModel {
|
||||||
|
|
||||||
constructor() : this(0, "", "", null, null, null)
|
constructor() : this(0, "", "", "", null, null, null, emptyList())
|
||||||
|
|
||||||
override fun notifyId(): Int = id.toInt()
|
override fun notifyId(): Int = id.toInt()
|
||||||
|
|
||||||
|
@ -50,6 +53,8 @@ data class Site(
|
||||||
|
|
||||||
override fun notifyTag(): String = url
|
override fun notifyTag(): String = url
|
||||||
|
|
||||||
|
override fun notifyDescription() = lastResult?.reason
|
||||||
|
|
||||||
fun intervalText(): String {
|
fun intervalText(): String {
|
||||||
requireNotNull(settings) { "Settings not queried." }
|
requireNotNull(settings) { "Settings not queried." }
|
||||||
val lastCheck = lastResult?.timestampMs ?: -1
|
val lastCheck = lastResult?.timestampMs ?: -1
|
||||||
|
|
|
@ -40,8 +40,10 @@ data class SiteSettings(
|
||||||
/** Whether or not the [Site] is enabled for automatic periodic checks. */
|
/** Whether or not the [Site] is enabled for automatic periodic checks. */
|
||||||
var disabled: Boolean,
|
var disabled: Boolean,
|
||||||
/** The network response timeout for validation attempts. */
|
/** The network response timeout for validation attempts. */
|
||||||
var networkTimeout: Int
|
var networkTimeout: Int,
|
||||||
|
/** The Uri to a self signed certificate. */
|
||||||
|
var certificate: String?
|
||||||
) : Serializable {
|
) : Serializable {
|
||||||
|
|
||||||
constructor() : this(0, 0, STATUS_CODE, null, false, 0)
|
constructor() : this(0, 0, STATUS_CODE, null, false, 0, null)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,52 +3,56 @@ ext.versions = [
|
||||||
minSdk : 21,
|
minSdk : 21,
|
||||||
compileSdk : 28,
|
compileSdk : 28,
|
||||||
buildTools : '28.0.3',
|
buildTools : '28.0.3',
|
||||||
publishVersion : '0.8.2b',
|
publishVersion : '0.8.8',
|
||||||
publishVersionCode : 33,
|
publishVersionCode : 46,
|
||||||
|
|
||||||
// Plugins
|
// Plugins
|
||||||
gradlePlugin : '3.2.1',
|
gradlePlugin : '3.4.0',
|
||||||
spotlessPlugin : '3.17.0',
|
spotlessPlugin : '3.22.0',
|
||||||
versionPlugin : '0.20.0',
|
versionPlugin : '0.21.0',
|
||||||
|
googleServices : '4.2.0',
|
||||||
fabricPlugin : '1.+',
|
fabricPlugin : '1.+',
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
okHttp : '3.12.1',
|
okHttp : '3.14.1',
|
||||||
rhino : '1.7.10',
|
rhino : '1.7.10',
|
||||||
|
|
||||||
// Kotlin
|
// Kotlin
|
||||||
kotlin : '1.3.11',
|
kotlin : '1.3.30',
|
||||||
coroutines : '1.1.0',
|
coroutines : '1.2.0',
|
||||||
koin : '1.0.2',
|
koin : '1.0.2',
|
||||||
|
|
||||||
// Google/AndroidX
|
// Google/AndroidX
|
||||||
androidxAnnotations : '1.0.1',
|
androidxAnnotations : '1.0.2',
|
||||||
androidxCore : '1.0.2',
|
androidxCore : '1.0.2',
|
||||||
androidxRecyclerView: '1.0.0',
|
androidxRecyclerView: '1.0.0',
|
||||||
androidxBrowser : '1.0.0',
|
androidxBrowser : '1.0.0',
|
||||||
googleMaterial : '1.0.0',
|
googleMaterial : '1.0.0',
|
||||||
room : '2.0.0',
|
room : '2.0.0',
|
||||||
lifecycle : '2.0.0',
|
lifecycle : '2.0.0',
|
||||||
|
firebaseCore : '16.0.8',
|
||||||
|
|
||||||
// Rx
|
// Rx
|
||||||
|
rxJava : '2.2.8',
|
||||||
rxBinding : '3.0.0-alpha1',
|
rxBinding : '3.0.0-alpha1',
|
||||||
|
|
||||||
// afollestad
|
// afollestad
|
||||||
materialDialogs : '2.0.0-rc7',
|
materialDialogs : '2.8.1',
|
||||||
rxkPrefs : '1.2.1',
|
rxkPrefs : '1.2.5',
|
||||||
|
vvalidator : '0.4.1',
|
||||||
|
|
||||||
// Debugging
|
// Debugging
|
||||||
timber : '4.7.1',
|
timber : '4.7.1',
|
||||||
fabric : '2.9.8@aar',
|
fabric : '2.9.9@aar',
|
||||||
|
|
||||||
// Unit testing
|
// Unit testing
|
||||||
junit : '4.12',
|
junit : '4.12',
|
||||||
mockito : '2.23.4',
|
mockito : '2.27.0',
|
||||||
mockitoKotlin : '2.0.0-RC1',
|
mockitoKotlin : '2.1.0',
|
||||||
truth : '0.42',
|
truth : '0.44',
|
||||||
|
|
||||||
// UI testing
|
// UI testing
|
||||||
androidxTestRunner : '1.1.1',
|
androidxTestRunner : '1.1.1',
|
||||||
androidxTest : '1.1.0',
|
androidxTest : '1.1.0',
|
||||||
archTesting : '2.0.0'
|
archTesting : '2.0.1'
|
||||||
]
|
]
|
||||||
|
|
|
@ -15,14 +15,18 @@
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.engine
|
package com.afollestad.nocknock.engine
|
||||||
|
|
||||||
import com.afollestad.nocknock.engine.validation.RealValidationManager
|
import com.afollestad.nocknock.engine.ssl.RealSslManager
|
||||||
import com.afollestad.nocknock.engine.validation.ValidationManager
|
import com.afollestad.nocknock.engine.ssl.SslManager
|
||||||
|
import com.afollestad.nocknock.engine.validation.RealValidationExecutor
|
||||||
|
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||||
import org.koin.dsl.module.module
|
import org.koin.dsl.module.module
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
val engineModule = module {
|
val engineModule = module {
|
||||||
|
|
||||||
single {
|
single {
|
||||||
RealValidationManager(get(), get(), get(), get(), get(), get())
|
RealValidationExecutor(get(), get(), get(), get(), get(), get(), get())
|
||||||
} bind ValidationManager::class
|
} bind ValidationExecutor::class
|
||||||
|
|
||||||
|
factory { RealSslManager(get()) } bind SslManager::class
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
/**
|
||||||
|
* 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) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class BootReceiver : BroadcastReceiver(), KoinComponent {
|
class BootReceiver : BroadcastReceiver(), KoinComponent {
|
||||||
|
|
||||||
private val validationManager by inject<ValidationManager>()
|
private val validationManager by inject<ValidationExecutor>()
|
||||||
private val mainDispatcher by inject<CoroutineDispatcher>(name = MAIN_DISPATCHER)
|
private val mainDispatcher by inject<CoroutineDispatcher>(name = MAIN_DISPATCHER)
|
||||||
private val ioDispatcher by inject<CoroutineDispatcher>(name = IO_DISPATCHER)
|
private val ioDispatcher by inject<CoroutineDispatcher>(name = IO_DISPATCHER)
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ class BootReceiver : BroadcastReceiver(), KoinComponent {
|
||||||
|
|
||||||
val pendingResult = goAsync()
|
val pendingResult = goAsync()
|
||||||
GlobalScope.launch(mainDispatcher) {
|
GlobalScope.launch(mainDispatcher) {
|
||||||
withContext(ioDispatcher) { validationManager.ensureScheduledChecks() }
|
withContext(ioDispatcher) { validationManager.ensureScheduledValidations() }
|
||||||
pendingResult.resultCode = 0
|
pendingResult.resultCode = 0
|
||||||
pendingResult.finish()
|
pendingResult.finish()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,21 +17,26 @@ package com.afollestad.nocknock.engine.validation
|
||||||
|
|
||||||
import android.app.job.JobScheduler
|
import android.app.job.JobScheduler
|
||||||
import android.app.job.JobScheduler.RESULT_SUCCESS
|
import android.app.job.JobScheduler.RESULT_SUCCESS
|
||||||
|
import android.net.Uri
|
||||||
import com.afollestad.nocknock.data.AppDatabase
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.data.allSites
|
import com.afollestad.nocknock.data.allSites
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.Status.ERROR
|
import com.afollestad.nocknock.data.model.Status.ERROR
|
||||||
import com.afollestad.nocknock.data.model.Status.OK
|
import com.afollestad.nocknock.data.model.Status.OK
|
||||||
import com.afollestad.nocknock.engine.R
|
import com.afollestad.nocknock.engine.R
|
||||||
|
import com.afollestad.nocknock.engine.ssl.SslManager
|
||||||
import com.afollestad.nocknock.engine.validation.ValidationJob.Companion.KEY_SITE_ID
|
import com.afollestad.nocknock.engine.validation.ValidationJob.Companion.KEY_SITE_ID
|
||||||
|
import com.afollestad.nocknock.utilities.ext.isNotNullOrEmpty
|
||||||
import com.afollestad.nocknock.utilities.providers.BundleProvider
|
import com.afollestad.nocknock.utilities.providers.BundleProvider
|
||||||
import com.afollestad.nocknock.utilities.providers.JobInfoProvider
|
import com.afollestad.nocknock.utilities.providers.JobInfoProvider
|
||||||
import com.afollestad.nocknock.utilities.providers.StringProvider
|
import com.afollestad.nocknock.utilities.providers.StringProvider
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import org.jetbrains.annotations.TestOnly
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||||
|
import kotlin.math.max
|
||||||
import timber.log.Timber.d as log
|
import timber.log.Timber.d as log
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
|
@ -42,12 +47,14 @@ data class CheckResult(
|
||||||
|
|
||||||
typealias ClientTimeoutChanger = (client: OkHttpClient, timeout: Int) -> OkHttpClient
|
typealias ClientTimeoutChanger = (client: OkHttpClient, timeout: Int) -> OkHttpClient
|
||||||
|
|
||||||
|
typealias UriConverter = (String) -> Uri
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
interface ValidationManager {
|
interface ValidationExecutor {
|
||||||
|
|
||||||
suspend fun ensureScheduledChecks()
|
suspend fun ensureScheduledValidations()
|
||||||
|
|
||||||
fun scheduleCheck(
|
fun scheduleValidation(
|
||||||
site: Site,
|
site: Site,
|
||||||
rightNow: Boolean = false,
|
rightNow: Boolean = false,
|
||||||
cancelPrevious: Boolean = rightNow,
|
cancelPrevious: Boolean = rightNow,
|
||||||
|
@ -55,19 +62,20 @@ interface ValidationManager {
|
||||||
overrideDelay: Long = -1
|
overrideDelay: Long = -1
|
||||||
)
|
)
|
||||||
|
|
||||||
fun cancelCheck(site: Site)
|
fun cancelScheduledValidation(site: Site)
|
||||||
|
|
||||||
suspend fun performCheck(site: Site): CheckResult
|
suspend fun performValidation(site: Site): CheckResult
|
||||||
}
|
}
|
||||||
|
|
||||||
class RealValidationManager(
|
class RealValidationExecutor(
|
||||||
private val jobScheduler: JobScheduler,
|
private val jobScheduler: JobScheduler,
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val bundleProvider: BundleProvider,
|
private val bundleProvider: BundleProvider,
|
||||||
private val jobInfoProvider: JobInfoProvider,
|
private val jobInfoProvider: JobInfoProvider,
|
||||||
private val database: AppDatabase
|
private val database: AppDatabase,
|
||||||
) : ValidationManager {
|
private val sslManager: SslManager
|
||||||
|
) : ValidationExecutor {
|
||||||
|
|
||||||
private var clientTimeoutChanger: ClientTimeoutChanger = { client, timeout ->
|
private var clientTimeoutChanger: ClientTimeoutChanger = { client, timeout ->
|
||||||
client.newBuilder()
|
client.newBuilder()
|
||||||
|
@ -75,37 +83,37 @@ class RealValidationManager(
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun ensureScheduledChecks() {
|
override suspend fun ensureScheduledValidations() {
|
||||||
val sites = database.allSites()
|
val sites = database.allSites()
|
||||||
if (sites.isEmpty()) {
|
if (sites.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log("Ensuring enabled sites have scheduled checks.")
|
log("Ensuring enabled sites have scheduled validations.")
|
||||||
sites.filter { it.settings?.disabled != true }
|
sites.filter { it.settings?.disabled != true }
|
||||||
.forEach { site ->
|
.forEach { site ->
|
||||||
val existingJob = jobForSite(site)
|
val existingJob = jobForSite(site)
|
||||||
if (existingJob == null) {
|
if (existingJob == null) {
|
||||||
log("Site ${site.id} does NOT have a scheduled job, running one now.")
|
log("Site ${site.id} does NOT have a scheduled job, running one now.")
|
||||||
scheduleCheck(site = site, rightNow = true)
|
scheduleValidation(site = site, rightNow = true)
|
||||||
} else {
|
} else {
|
||||||
log("Site ${site.id} already has a scheduled job. Nothing to do.")
|
log("Site ${site.id} already has a scheduled job. Nothing to do.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun scheduleCheck(
|
override fun scheduleValidation(
|
||||||
site: Site,
|
site: Site,
|
||||||
rightNow: Boolean,
|
rightNow: Boolean,
|
||||||
cancelPrevious: Boolean,
|
cancelPrevious: Boolean,
|
||||||
fromFinishingJob: Boolean,
|
fromFinishingJob: Boolean,
|
||||||
overrideDelay: Long
|
overrideDelay: Long
|
||||||
) {
|
) {
|
||||||
check(site.id != 0L) { "Cannot schedule checks for jobs with no ID." }
|
check(site.id != 0L) { "Cannot schedule validations for jobs with no ID." }
|
||||||
val siteSettings = site.settings
|
val siteSettings = site.settings
|
||||||
requireNotNull(siteSettings) { "Site settings must be populated." }
|
requireNotNull(siteSettings) { "Site settings must be populated." }
|
||||||
|
|
||||||
if (cancelPrevious) {
|
if (cancelPrevious) {
|
||||||
cancelCheck(site)
|
cancelScheduledValidation(site)
|
||||||
} else if (!fromFinishingJob) {
|
} else if (!fromFinishingJob) {
|
||||||
val existingJob = jobForSite(site)
|
val existingJob = jobForSite(site)
|
||||||
check(existingJob == null) {
|
check(existingJob == null) {
|
||||||
|
@ -113,7 +121,7 @@ class RealValidationManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Requesting a check job for site to be scheduled: $site")
|
log("Requesting a validation job for site to be scheduled: $site")
|
||||||
val extras = bundleProvider.createPersistable {
|
val extras = bundleProvider.createPersistable {
|
||||||
putLong(KEY_SITE_ID, site.id)
|
putLong(KEY_SITE_ID, site.id)
|
||||||
}
|
}
|
||||||
|
@ -131,43 +139,59 @@ class RealValidationManager(
|
||||||
|
|
||||||
val dispatchResult = jobScheduler.schedule(jobInfo)
|
val dispatchResult = jobScheduler.schedule(jobInfo)
|
||||||
if (dispatchResult != RESULT_SUCCESS) {
|
if (dispatchResult != RESULT_SUCCESS) {
|
||||||
log("Failed to schedule a check job for site: ${site.id}")
|
log("Failed to schedule a validation job for site: ${site.id}")
|
||||||
} else {
|
} else {
|
||||||
log("Check job successfully scheduled for site: ${site.id}")
|
log("Validation job successfully scheduled for site: ${site.id}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelCheck(site: Site) {
|
override fun cancelScheduledValidation(site: Site) {
|
||||||
check(site.id != 0L) { "Cannot cancel scheduled checks for jobs with no ID." }
|
check(site.id != 0L) { "Cannot cancel scheduled validations for jobs with no ID." }
|
||||||
log("Cancelling scheduled checks for site: ${site.id}")
|
log("Cancelling scheduled validations for site: ${site.id}")
|
||||||
jobScheduler.cancel(site.id.toInt())
|
jobScheduler.cancel(site.id.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun performCheck(site: Site): CheckResult {
|
override suspend fun performValidation(site: Site): CheckResult {
|
||||||
check(site.id != 0L) { "Cannot schedule checks for jobs with no ID." }
|
check(site.id != 0L) { "Cannot schedule validations for jobs with no ID." }
|
||||||
val siteSettings = site.settings
|
val siteSettings = site.settings
|
||||||
requireNotNull(siteSettings) { "Site settings must be populated." }
|
requireNotNull(siteSettings) { "Site settings must be populated." }
|
||||||
check(siteSettings.networkTimeout > 0) { "Network timeout not set for site ${site.id}" }
|
log("performValidation(${site.id}) - GET ${site.url}")
|
||||||
log("performCheck(${site.id}) - GET ${site.url}")
|
|
||||||
|
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(site.url)
|
.apply {
|
||||||
.get()
|
url(site.url)
|
||||||
|
get()
|
||||||
|
site.headers
|
||||||
|
.filter { header -> header.key.isNotNullOrEmpty() }
|
||||||
|
.forEach { header ->
|
||||||
|
addHeader(header.key, header.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val client = clientTimeoutChanger(okHttpClient, siteSettings.networkTimeout)
|
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 response = client.newCall(request)
|
val response = client.newCall(request)
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
if (response.isSuccessful || response.code() == 401) {
|
if (response.isSuccessful) {
|
||||||
log("performCheck(${site.id}) = Successful")
|
log("performValidation(${site.id}) = Successful")
|
||||||
CheckResult(
|
CheckResult(
|
||||||
model = site.withStatus(status = OK, reason = null),
|
model = site.withStatus(status = OK, reason = null),
|
||||||
response = response
|
response = response
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
log("performCheck(${site.id}) = Failure, HTTP code ${response.code()}")
|
log("performValidation(${site.id}) = Failure, HTTP code ${response.code()}")
|
||||||
CheckResult(
|
CheckResult(
|
||||||
model = site.withStatus(
|
model = site.withStatus(
|
||||||
status = ERROR,
|
status = ERROR,
|
||||||
|
@ -177,7 +201,7 @@ class RealValidationManager(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (timeoutEx: SocketTimeoutException) {
|
} catch (timeoutEx: SocketTimeoutException) {
|
||||||
log("performCheck(${site.id}) = Socket Timeout")
|
log("performValidation(${site.id}) = Socket Timeout")
|
||||||
CheckResult(
|
CheckResult(
|
||||||
model = site.withStatus(
|
model = site.withStatus(
|
||||||
status = ERROR,
|
status = ERROR,
|
||||||
|
@ -185,7 +209,8 @@ class RealValidationManager(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
log("performCheck(${site.id}) = Error: ${ex.message}")
|
ex.printStackTrace()
|
||||||
|
log("performValidation(${site.id}) = Error: ${ex.message}")
|
||||||
CheckResult(model = site.withStatus(status = ERROR, reason = ex.message))
|
CheckResult(model = site.withStatus(status = ERROR, reason = ex.message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -194,7 +219,7 @@ class RealValidationManager(
|
||||||
jobScheduler.allPendingJobs
|
jobScheduler.allPendingJobs
|
||||||
.firstOrNull { job -> job.id == site.id.toInt() }
|
.firstOrNull { job -> job.id == site.id.toInt() }
|
||||||
|
|
||||||
// @TestOnly fun setClientTimeoutChanger(changer: ClientTimeoutChanger) {
|
@TestOnly fun setClientTimeoutChanger(changer: ClientTimeoutChanger) {
|
||||||
// this.clientTimeoutChanger = changer
|
this.clientTimeoutChanger = changer
|
||||||
// }
|
}
|
||||||
}
|
}
|
|
@ -19,8 +19,8 @@ import android.app.job.JobParameters
|
||||||
import android.app.job.JobService
|
import android.app.job.JobService
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.afollestad.nocknock.data.AppDatabase
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.data.RetryPolicy
|
|
||||||
import com.afollestad.nocknock.data.getSite
|
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.Site
|
||||||
import com.afollestad.nocknock.data.model.Status
|
import com.afollestad.nocknock.data.model.Status
|
||||||
import com.afollestad.nocknock.data.model.Status.CHECKING
|
import com.afollestad.nocknock.data.model.Status.CHECKING
|
||||||
|
@ -60,7 +60,7 @@ class ValidationJob : JobService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private val database by inject<AppDatabase>()
|
private val database by inject<AppDatabase>()
|
||||||
private val validationManager by inject<ValidationManager>()
|
private val validationManager by inject<ValidationExecutor>()
|
||||||
private val notificationManager by inject<NockNotificationManager>()
|
private val notificationManager by inject<NockNotificationManager>()
|
||||||
|
|
||||||
override fun onStartJob(params: JobParameters): Boolean {
|
override fun onStartJob(params: JobParameters): Boolean {
|
||||||
|
@ -80,10 +80,14 @@ class ValidationJob : JobService() {
|
||||||
sendBroadcast(Intent(ACTION_JOB_RUNNING).apply { putExtra(KEY_SITE_ID, site.id) })
|
sendBroadcast(Intent(ACTION_JOB_RUNNING).apply { putExtra(KEY_SITE_ID, site.id) })
|
||||||
|
|
||||||
log("Checking ${site.name} (${site.url})...")
|
log("Checking ${site.name} (${site.url})...")
|
||||||
|
val lastResult = site.lastResult
|
||||||
|
if (lastResult != null) {
|
||||||
|
log("Result of previous attempt: ${lastResult.status}")
|
||||||
|
}
|
||||||
|
|
||||||
val jobResult = async(IO) {
|
val jobResult = async(IO) {
|
||||||
updateStatus(site, CHECKING)
|
updateStatus(site, CHECKING)
|
||||||
val checkResult = validationManager.performCheck(site)
|
val checkResult = validationManager.performValidation(site)
|
||||||
val resultModel = checkResult.model
|
val resultModel = checkResult.model
|
||||||
val resultResponse = checkResult.response
|
val resultResponse = checkResult.response
|
||||||
val result = resultModel.lastResult!!
|
val result = resultModel.lastResult!!
|
||||||
|
@ -139,6 +143,9 @@ class ValidationJob : JobService() {
|
||||||
|
|
||||||
if (jobResult.lastResult!!.status == OK) {
|
if (jobResult.lastResult!!.status == OK) {
|
||||||
notificationManager.cancelStatusNotification(jobResult)
|
notificationManager.cancelStatusNotification(jobResult)
|
||||||
|
if (lastResult != null && lastResult.status == ERROR) {
|
||||||
|
notificationManager.postValidationSuccessNotification(jobResult)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val retryPolicy = site.retryPolicy
|
val retryPolicy = site.retryPolicy
|
||||||
if (retryPolicy != null) {
|
if (retryPolicy != null) {
|
||||||
|
@ -153,7 +160,7 @@ class ValidationJob : JobService() {
|
||||||
updateTriesLeft(retryPolicy, retryPolicy.triesLeft)
|
updateTriesLeft(retryPolicy, retryPolicy.triesLeft)
|
||||||
|
|
||||||
val interval = retryPolicy.interval()
|
val interval = retryPolicy.interval()
|
||||||
validationManager.scheduleCheck(
|
validationManager.scheduleValidation(
|
||||||
site = jobResult,
|
site = jobResult,
|
||||||
fromFinishingJob = true,
|
fromFinishingJob = true,
|
||||||
overrideDelay = interval
|
overrideDelay = interval
|
||||||
|
@ -167,10 +174,10 @@ class ValidationJob : JobService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationManager.postStatusNotification(jobResult)
|
notificationManager.postValidationErrorNotification(jobResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
validationManager.scheduleCheck(
|
validationManager.scheduleValidation(
|
||||||
site = jobResult,
|
site = jobResult,
|
||||||
fromFinishingJob = true
|
fromFinishingJob = true
|
||||||
)
|
)
|
||||||
|
@ -225,6 +232,7 @@ class ValidationJob : JobService() {
|
||||||
triesLeft: Int
|
triesLeft: Int
|
||||||
) {
|
) {
|
||||||
retryPolicy.triesLeft = triesLeft
|
retryPolicy.triesLeft = triesLeft
|
||||||
|
retryPolicy.lastTryTimestamp = currentTimeMillis()
|
||||||
withContext(IO) {
|
withContext(IO) {
|
||||||
database.retryPolicyDao()
|
database.retryPolicyDao()
|
||||||
.update(retryPolicy)
|
.update(retryPolicy)
|
||||||
|
|