Compare commits
114 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 | ||
|
8a4c7448f5 | ||
|
6ff3dfc214 | ||
|
5cd7127e4f | ||
|
b153622a93 | ||
|
2585ed77b9 | ||
|
69d9eb094e | ||
|
31c9e94e15 | ||
|
da7623db79 | ||
|
09352724ee | ||
|
19f58ef2e6 | ||
|
6daebe46eb | ||
|
2f4c7126db | ||
|
aa4bebf1ce | ||
|
1d2b79d5a3 | ||
|
44d31dd5c3 | ||
|
1569871524 | ||
|
de36a2f5e6 | ||
|
84eb0b30e1 | ||
|
48735bb606 | ||
|
a8d7f85c9b | ||
|
4c3c22eb8d | ||
|
a8626b4d29 | ||
|
b7c51a12ed | ||
|
b09088ab9e | ||
|
8effe38a1a | ||
|
3b1aae66f3 | ||
|
76a5a46454 | ||
|
fc6bdf1c39 | ||
|
9a849ab8ac | ||
|
8470b65df1 | ||
|
b57f645c98 | ||
|
98327c8c5b | ||
|
1e92644904 | ||
|
c9750f5f66 | ||
|
88ae30c0c9 | ||
|
cad589eebc | ||
|
f9711137b9 | ||
|
38d7bcb7f9 | ||
|
62ef385b65 | ||
|
7e46b84d08 | ||
|
92878c875e | ||
|
8a1816c3e8 | ||
|
14a86568e6 | ||
|
b8dd2c0d24 | ||
|
03c687def5 |
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
|
||||
|
||||
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.
|
||||
3. A PR should fix a bug or solve a problem - something that only you would use is not necessarily something that should be published.
|
||||
4. Give your PR a detailed title and description - look over your code one last time before actually creating the PR. Give it a self-review.
|
||||
|
||||
**If you do not follow the guidelines, your PR will be rejected.**
|
||||
**If you do not follow the guidelines, your PR will be rejected.**
|
4
.gitignore
vendored
|
@ -180,4 +180,6 @@ gradle-app.setting
|
|||
.gradletasknamecache
|
||||
|
||||
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||
# gradle/wrapper/gradle-wrapper.properties
|
||||
# gradle/wrapper/gradle-wrapper.properties
|
||||
|
||||
app/google-services.json
|
17
.idea/misc.xml
generated
|
@ -1,11 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CMakeSettings">
|
||||
<configurations>
|
||||
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
|
||||
</configurations>
|
||||
</component>
|
||||
<component name="NullableNotNullManager">
|
||||
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
|
||||
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
|
||||
<option name="myNullables">
|
||||
<value>
|
||||
<list size="7">
|
||||
<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" />
|
||||
|
@ -13,23 +18,29 @@
|
|||
<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="6">
|
||||
<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_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
|
2
.idea/modules.xml
generated
|
@ -3,11 +3,11 @@
|
|||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/common/common.iml" filepath="$PROJECT_DIR$/common/common.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/data/data.iml" filepath="$PROJECT_DIR$/data/data.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/engine/engine.iml" filepath="$PROJECT_DIR$/engine/engine.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/nock-nock.iml" filepath="$PROJECT_DIR$/nock-nock.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/notifications/notifications.iml" filepath="$PROJECT_DIR$/notifications/notifications.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/utilities/utilities.iml" filepath="$PROJECT_DIR$/utilities/utilities.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/viewcomponents/viewcomponents.iml" filepath="$PROJECT_DIR$/viewcomponents/viewcomponents.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
|
|
22
.travis.yml
|
@ -1,22 +0,0 @@
|
|||
language: android
|
||||
jdk: oraclejdk8
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-28.0.3
|
||||
- android-28
|
||||
- extra-android-support
|
||||
- extra-android-m2repository
|
||||
- extra-google-m2repository
|
||||
|
||||
# Additional components
|
||||
#- extra-google-google_play_services
|
||||
#- addon-google_apis-google-19
|
||||
|
||||
# Specify at least one system image, if you need to run emulator(s) during your tests
|
||||
#- sys-img-armeabi-v7a-android-19
|
||||
#- sys-img-x86-android-17
|
||||
|
||||
licenses:
|
||||
- '.+'
|
|
@ -1,9 +1,8 @@
|
|||
## Nock Nock
|
||||
|
||||
[](https://travis-ci.org/afollestad/nock-nock)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
|
||||

|
||||

|
||||
|
||||
Nock Nock is a simple app which allows you to monitor your websites for maximum uptime.
|
||||
|
||||
|
@ -11,4 +10,4 @@ The app will automatically knock on the door of your websites (or web servers) o
|
|||
to make sure they are up and responding successfully. If something is wrong, you get a notification telling you so.
|
||||
|
||||
<br/>
|
||||
<a href="https://play.google.com/store/apps/details?id=com.afollestad.nocknock&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="200px"/></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=com.afollestad.nocknock&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="200px"/></a>
|
||||
|
|
|
@ -15,30 +15,65 @@ android {
|
|||
versionCode versions.publishVersionCode
|
||||
versionName versions.publishVersion
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility 1.8
|
||||
targetCompatibility 1.8
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/atomicfu.kotlin_module'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':data')
|
||||
implementation project(':utilities')
|
||||
implementation project(':common')
|
||||
implementation project(':engine')
|
||||
implementation project(':data')
|
||||
implementation project(':notifications')
|
||||
implementation project(':viewcomponents')
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:' + versions.androidx
|
||||
implementation 'androidx.recyclerview:recyclerview:' + versions.androidx
|
||||
implementation 'com.google.android.material:material:' + versions.androidx
|
||||
// Google/AppCompat
|
||||
implementation 'androidx.appcompat:appcompat:' + versions.androidxCore
|
||||
implementation 'androidx.recyclerview:recyclerview:' + versions.androidxRecyclerView
|
||||
implementation 'com.google.android.material:material:' + versions.googleMaterial
|
||||
implementation 'androidx.browser:browser:' + versions.androidxBrowser
|
||||
implementation 'com.google.firebase:firebase-core:' + versions.firebaseCore
|
||||
|
||||
// Lifecycle
|
||||
kapt 'androidx.lifecycle:lifecycle-compiler:' + versions.lifecycle
|
||||
|
||||
// Kotlin
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin
|
||||
|
||||
implementation 'com.google.dagger:dagger:' + versions.dagger
|
||||
kapt 'com.google.dagger:dagger-compiler:' + versions.dagger
|
||||
// JOIN
|
||||
implementation 'org.koin:koin-android:' + versions.koin
|
||||
implementation 'org.koin:koin-androidx-scope:' + versions.koin
|
||||
implementation 'org.koin:koin-androidx-viewmodel:' + versions.koin
|
||||
|
||||
// afollestad
|
||||
implementation 'com.afollestad.material-dialogs:core:' + versions.materialDialogs
|
||||
|
||||
// Debugging
|
||||
implementation 'com.jakewharton.timber:timber:' + versions.timber
|
||||
implementation("com.crashlytics.sdk.android:crashlytics:${versions.fabric}") {
|
||||
transitive = true
|
||||
}
|
||||
|
||||
// Testing
|
||||
testImplementation 'junit:junit:' + versions.junit
|
||||
testImplementation 'org.mockito:mockito-core:' + versions.mockito
|
||||
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:' + versions.mockitoKotlin
|
||||
testImplementation 'com.google.truth:truth:' + versions.truth
|
||||
testImplementation 'androidx.arch.core:core-testing:' + versions.archTesting
|
||||
|
||||
// UI testing
|
||||
androidTestImplementation 'androidx.test:runner:' + 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'
|
|
@ -31,22 +31,20 @@
|
|||
android:name="com.afollestad.nocknock.ui.addsite.AddSiteActivity"
|
||||
android:label="@string/add_site"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/AppTheme.Transparent"
|
||||
android:windowSoftInputMode="stateHidden"/>
|
||||
|
||||
<activity
|
||||
android:name="com.afollestad.nocknock.ui.viewsite.ViewSiteActivity"
|
||||
android:label="@string/view_site"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/AppTheme.Ink"
|
||||
android:windowSoftInputMode="stateHidden"/>
|
||||
|
||||
<service
|
||||
android:name=".engine.statuscheck.CheckStatusJob"
|
||||
android:name=".engine.validation.ValidationJob"
|
||||
android:label="@string/check_service_name"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"/>
|
||||
|
||||
<receiver android:name=".engine.statuscheck.BootReceiver">
|
||||
<receiver android:name=".engine.validation.BootReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
|
|
|
@ -1,16 +1,32 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.app.Application.ActivityLifecycleCallbacks
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||
import androidx.core.text.HtmlCompat.fromHtml
|
||||
import com.afollestad.materialdialogs.utils.MDUtil.resolveColor
|
||||
import com.afollestad.nocknock.utilities.ext.toUri
|
||||
import com.afollestad.nocknock.utilities.ui.toast
|
||||
|
||||
typealias ActivityLifeChange = (activity: Activity, resumed: Boolean) -> Unit
|
||||
|
||||
|
@ -40,3 +56,37 @@ fun Application.onActivityLifeChange(cb: ActivityLifeChange) {
|
|||
}
|
||||
|
||||
fun String.toHtml() = fromHtml(this, FROM_HTML_MODE_LEGACY)
|
||||
|
||||
fun Activity.viewUrl(url: String) {
|
||||
val customTabsIntent = CustomTabsIntent.Builder()
|
||||
.apply {
|
||||
setToolbarColor(resolveColor(this@viewUrl, attr = R.attr.colorPrimary))
|
||||
}
|
||||
.build()
|
||||
try {
|
||||
customTabsIntent.launchUrl(this, url.toUri())
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
toast(R.string.install_web_browser)
|
||||
}
|
||||
}
|
||||
|
||||
fun Activity.viewUrlWithApp(
|
||||
url: String,
|
||||
pkg: String
|
||||
) {
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
data = url.toUri()
|
||||
}
|
||||
val resInfo = packageManager.queryIntentActivities(intent, 0)
|
||||
for (info in resInfo) {
|
||||
if (info.activityInfo.packageName.toLowerCase().contains(pkg) ||
|
||||
info.activityInfo.name.toLowerCase().contains(pkg)
|
||||
) {
|
||||
startActivity(intent.apply {
|
||||
setPackage(info.activityInfo.packageName)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
viewUrl(url)
|
||||
}
|
||||
|
|
|
@ -1,62 +1,69 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@file:Suppress("unused")
|
||||
|
||||
package com.afollestad.nocknock
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import com.afollestad.nocknock.di.AppComponent
|
||||
import com.afollestad.nocknock.di.DaggerAppComponent
|
||||
import com.afollestad.nocknock.engine.statuscheck.BootReceiver
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob
|
||||
import com.afollestad.nocknock.BuildConfig.DEBUG
|
||||
import com.afollestad.nocknock.engine.engineModule
|
||||
import com.afollestad.nocknock.koin.mainModule
|
||||
import com.afollestad.nocknock.koin.prefModule
|
||||
import com.afollestad.nocknock.koin.viewModelModule
|
||||
import com.afollestad.nocknock.logging.FabricTree
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
||||
import com.afollestad.nocknock.ui.main.MainActivity
|
||||
import com.afollestad.nocknock.ui.viewsite.ViewSiteActivity
|
||||
import com.afollestad.nocknock.utilities.Injector
|
||||
import com.afollestad.nocknock.utilities.ext.systemService
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Inject
|
||||
import com.afollestad.nocknock.notifications.notificationsModule
|
||||
import com.afollestad.nocknock.utilities.commonModule
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import io.fabric.sdk.android.Fabric
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.ext.android.startKoin
|
||||
import timber.log.Timber
|
||||
import timber.log.Timber.DebugTree
|
||||
import timber.log.Timber.d as log
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class NockNockApp : Application(), Injector {
|
||||
|
||||
companion object {
|
||||
private fun log(message: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d("NockNockApp", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var appComponent: AppComponent
|
||||
@Inject lateinit var nockNotificationManager: NockNotificationManager
|
||||
class NockNockApp : Application() {
|
||||
|
||||
private var resumedActivities: Int = 0
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val okHttpClient = OkHttpClient.Builder()
|
||||
.addNetworkInterceptor { chain ->
|
||||
val request = chain.request()
|
||||
.newBuilder()
|
||||
.addHeader("User-Agent", "com.afollestad.nocknock")
|
||||
.build()
|
||||
chain.proceed(request)
|
||||
}
|
||||
.build()
|
||||
if (DEBUG) {
|
||||
Timber.plant(DebugTree())
|
||||
}
|
||||
|
||||
appComponent = DaggerAppComponent.builder()
|
||||
.application(this)
|
||||
.okHttpClient(okHttpClient)
|
||||
.jobScheduler(systemService(JOB_SCHEDULER_SERVICE))
|
||||
.notificationManager(systemService(NOTIFICATION_SERVICE))
|
||||
.build()
|
||||
appComponent.inject(this)
|
||||
Timber.plant(FabricTree())
|
||||
Fabric.with(this, Crashlytics())
|
||||
|
||||
val modules = listOf(
|
||||
prefModule,
|
||||
mainModule,
|
||||
engineModule,
|
||||
commonModule,
|
||||
notificationsModule,
|
||||
viewModelModule
|
||||
)
|
||||
startKoin(
|
||||
androidContext = this,
|
||||
modules = modules
|
||||
)
|
||||
|
||||
val nockNotificationManager by inject<NockNotificationManager>()
|
||||
onActivityLifeChange { activity, resumed ->
|
||||
if (resumed) {
|
||||
resumedActivities++
|
||||
|
@ -69,13 +76,4 @@ class NockNockApp : Application(), Injector {
|
|||
nockNotificationManager.setIsAppOpen(resumedActivities > 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun injectInto(target: Any) = when (target) {
|
||||
is MainActivity -> appComponent.inject(target)
|
||||
is ViewSiteActivity -> appComponent.inject(target)
|
||||
is AddSiteActivity -> appComponent.inject(target)
|
||||
is CheckStatusJob -> appComponent.inject(target)
|
||||
is BootReceiver -> appComponent.inject(target)
|
||||
else -> throw IllegalStateException("Can't inject into $target")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,30 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil.calculateDiff
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.isPending
|
||||
import com.afollestad.nocknock.data.textRes
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
import com.afollestad.nocknock.data.model.isPending
|
||||
import com.afollestad.nocknock.data.model.textRes
|
||||
import com.afollestad.nocknock.utilities.ui.onDebouncedClick
|
||||
import kotlinx.android.synthetic.main.list_item_server.view.iconStatus
|
||||
import kotlinx.android.synthetic.main.list_item_server.view.textInterval
|
||||
|
@ -20,12 +32,12 @@ import kotlinx.android.synthetic.main.list_item_server.view.textName
|
|||
import kotlinx.android.synthetic.main.list_item_server.view.textStatus
|
||||
import kotlinx.android.synthetic.main.list_item_server.view.textUrl
|
||||
|
||||
typealias Listener = (model: ServerModel, longClick: Boolean) -> Unit
|
||||
typealias Listener = (model: Site, longClick: Boolean) -> Unit
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class ServerVH constructor(
|
||||
class SiteViewHolder constructor(
|
||||
itemView: View,
|
||||
private val adapter: ServerAdapter
|
||||
private val adapter: SiteAdapter
|
||||
) : RecyclerView.ViewHolder(itemView), View.OnLongClickListener {
|
||||
|
||||
init {
|
||||
|
@ -35,24 +47,32 @@ class ServerVH constructor(
|
|||
itemView.setOnLongClickListener(this)
|
||||
}
|
||||
|
||||
fun bind(model: ServerModel) {
|
||||
fun bind(model: Site) {
|
||||
requireNotNull(model.settings) { "Settings must be populated." }
|
||||
|
||||
itemView.textName.text = model.name
|
||||
itemView.textUrl.text = model.url
|
||||
itemView.iconStatus.setStatus(model.status)
|
||||
|
||||
val statusText = model.status.textRes()
|
||||
if (statusText == 0) {
|
||||
itemView.textStatus.text = model.reason
|
||||
val lastResult = model.lastResult
|
||||
if (lastResult != null) {
|
||||
itemView.iconStatus.setStatus(lastResult.status)
|
||||
val statusText = lastResult.status.textRes()
|
||||
if (statusText == 0) {
|
||||
itemView.textStatus.text = lastResult.reason
|
||||
} else {
|
||||
itemView.textStatus.setText(statusText)
|
||||
}
|
||||
} else {
|
||||
itemView.textStatus.setText(statusText)
|
||||
itemView.iconStatus.setStatus(WAITING)
|
||||
itemView.textStatus.setText(R.string.none)
|
||||
}
|
||||
|
||||
val res = itemView.resources
|
||||
when {
|
||||
model.disabled -> {
|
||||
model.settings?.disabled == true -> {
|
||||
itemView.textInterval.setText(R.string.checks_disabled)
|
||||
}
|
||||
model.status.isPending() -> {
|
||||
model.lastResult?.status.isPending() -> {
|
||||
itemView.textInterval.text = res.getString(
|
||||
R.string.next_check_x,
|
||||
res.getString(R.string.now)
|
||||
|
@ -74,70 +94,33 @@ class ServerVH constructor(
|
|||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter<ServerVH>() {
|
||||
class SiteAdapter(private val listener: Listener) : RecyclerView.Adapter<SiteViewHolder>() {
|
||||
|
||||
private val models = mutableListOf<ServerModel>()
|
||||
private var models = mutableListOf<Site>()
|
||||
|
||||
internal fun performClick(
|
||||
index: Int,
|
||||
longClick: Boolean
|
||||
) = listener.invoke(models[index], longClick)
|
||||
|
||||
fun add(model: ServerModel) {
|
||||
models.add(model)
|
||||
notifyItemInserted(models.size - 1)
|
||||
}
|
||||
|
||||
fun update(target: ServerModel) {
|
||||
for ((i, model) in models.withIndex()) {
|
||||
if (model.id == target.id) {
|
||||
update(i, target)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(
|
||||
index: Int,
|
||||
model: ServerModel
|
||||
) {
|
||||
models[index] = model
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
|
||||
fun remove(index: Int) {
|
||||
models.removeAt(index)
|
||||
notifyItemRemoved(index)
|
||||
}
|
||||
|
||||
fun remove(target: ServerModel) {
|
||||
for ((i, model) in models.withIndex()) {
|
||||
if (model.id == target.id) {
|
||||
remove(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun set(newModels: List<ServerModel>) {
|
||||
this.models.clear()
|
||||
if (!newModels.isEmpty()) {
|
||||
this.models.addAll(newModels)
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
fun set(newModels: List<Site>) {
|
||||
val formerModels = this.models
|
||||
this.models = newModels.toMutableList()
|
||||
val diffResult = calculateDiff(SiteDiffCallback(formerModels, this.models))
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): ServerVH {
|
||||
): SiteViewHolder {
|
||||
val v = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.list_item_server, parent, false)
|
||||
return ServerVH(v, this)
|
||||
return SiteViewHolder(v, this)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ServerVH,
|
||||
holder: SiteViewHolder,
|
||||
position: Int
|
||||
) {
|
||||
val model = models[position]
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* 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 androidx.recyclerview.widget.DiffUtil
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class SiteDiffCallback(
|
||||
private val oldItems: List<Site>,
|
||||
private val newItems: List<Site>
|
||||
) : DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize() = oldItems.size
|
||||
|
||||
override fun getNewListSize() = newItems.size
|
||||
|
||||
override fun areItemsTheSame(
|
||||
oldItemPosition: Int,
|
||||
newItemPosition: Int
|
||||
) = oldItems[oldItemPosition].id == newItems[newItemPosition].id
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItemPosition: Int,
|
||||
newItemPosition: Int
|
||||
) = oldItems[oldItemPosition] == newItems[newItemPosition]
|
||||
}
|
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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* 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.broadcasts
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.Lifecycle.Event.ON_DESTROY
|
||||
import androidx.lifecycle.Lifecycle.Event.ON_PAUSE
|
||||
import androidx.lifecycle.Lifecycle.Event.ON_RESUME
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.engine.validation.ValidationJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.validation.ValidationJob.Companion.KEY_UPDATE_MODEL
|
||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||
|
||||
typealias SiteCallback = (Site) -> Unit
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class StatusUpdateIntentReceiver(
|
||||
private val context: Context,
|
||||
private val intentProvider: IntentProvider,
|
||||
private var callback: SiteCallback?
|
||||
) : LifecycleObserver {
|
||||
|
||||
internal val intentReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
intent: Intent
|
||||
) {
|
||||
if (intent.action == ACTION_STATUS_UPDATE) {
|
||||
val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? Site
|
||||
?: return
|
||||
callback?.invoke(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(ON_RESUME)
|
||||
fun onResume() {
|
||||
val filter = intentProvider.createFilter(ACTION_STATUS_UPDATE)
|
||||
context.registerReceiver(intentReceiver, filter)
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(ON_PAUSE)
|
||||
fun onPause() {
|
||||
context.unregisterReceiver(intentReceiver)
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(ON_DESTROY)
|
||||
fun onDestroy() {
|
||||
callback = null
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.di
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.app.job.JobScheduler
|
||||
import com.afollestad.nocknock.NockNockApp
|
||||
import com.afollestad.nocknock.engine.EngineModule
|
||||
import com.afollestad.nocknock.engine.statuscheck.BootReceiver
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob
|
||||
import com.afollestad.nocknock.notifications.NotificationsModule
|
||||
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
||||
import com.afollestad.nocknock.ui.main.MainActivity
|
||||
import com.afollestad.nocknock.ui.viewsite.ViewSiteActivity
|
||||
import com.afollestad.nocknock.utilities.UtilitiesModule
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Singleton
|
||||
@Component(
|
||||
modules = [
|
||||
MainModule::class,
|
||||
MainBindModule::class,
|
||||
EngineModule::class,
|
||||
NotificationsModule::class,
|
||||
UtilitiesModule::class
|
||||
]
|
||||
)
|
||||
interface AppComponent {
|
||||
|
||||
fun inject(app: NockNockApp)
|
||||
|
||||
fun inject(activity: MainActivity)
|
||||
|
||||
fun inject(activity: ViewSiteActivity)
|
||||
|
||||
fun inject(activity: AddSiteActivity)
|
||||
|
||||
fun inject(job: CheckStatusJob)
|
||||
|
||||
fun inject(bootReceiver: BootReceiver)
|
||||
|
||||
@Component.Builder
|
||||
interface Builder {
|
||||
|
||||
@BindsInstance fun application(application: Application): Builder
|
||||
|
||||
@BindsInstance fun okHttpClient(okHttpClient: OkHttpClient): Builder
|
||||
|
||||
@BindsInstance fun jobScheduler(jobScheduler: JobScheduler): Builder
|
||||
|
||||
@BindsInstance fun notificationManager(notificationManager: NotificationManager): Builder
|
||||
|
||||
fun build(): AppComponent
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.di
|
||||
|
||||
import com.afollestad.nocknock.ui.addsite.AddSitePresenter
|
||||
import com.afollestad.nocknock.ui.addsite.RealAddSitePresenter
|
||||
import com.afollestad.nocknock.ui.main.MainPresenter
|
||||
import com.afollestad.nocknock.ui.main.RealMainPresenter
|
||||
import com.afollestad.nocknock.ui.viewsite.RealViewSitePresenter
|
||||
import com.afollestad.nocknock.ui.viewsite.ViewSitePresenter
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Module
|
||||
abstract class MainBindModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideMainPresenter(
|
||||
presenter: RealMainPresenter
|
||||
): MainPresenter
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideAddSitePresenter(
|
||||
presenter: RealAddSitePresenter
|
||||
): AddSitePresenter
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideViewSitePresenter(
|
||||
presenter: RealViewSitePresenter
|
||||
): ViewSitePresenter
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.di
|
||||
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.ui.main.MainActivity
|
||||
import com.afollestad.nocknock.utilities.qualifiers.AppIconRes
|
||||
import com.afollestad.nocknock.utilities.qualifiers.MainActivityClass
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Module
|
||||
open class MainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@AppIconRes
|
||||
fun provideAppIconRes(): Int = R.mipmap.ic_launcher
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@MainActivityClass
|
||||
fun provideMainActivityClass(): Class<*> = MainActivity::class.java
|
||||
}
|
|
@ -1,7 +1,17 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.dialogs
|
||||
|
||||
|
@ -10,6 +20,7 @@ import android.os.Bundle
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.nocknock.BuildConfig
|
||||
import com.afollestad.nocknock.R
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
|
@ -24,8 +35,9 @@ class AboutDialog : DialogFragment() {
|
|||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return MaterialDialog(activity!!)
|
||||
.title(R.string.about)
|
||||
val context = activity ?: throw IllegalStateException("Oh no!")
|
||||
return MaterialDialog(context)
|
||||
.title(text = getString(R.string.app_name_x, BuildConfig.VERSION_NAME))
|
||||
.positiveButton(R.string.dismiss)
|
||||
.message(R.string.about_body, html = true, lineHeightMultiplier = 1.4f)
|
||||
}
|
||||
|
|
72
app/src/main/java/com/afollestad/nocknock/koin/MainModule.kt
Normal file
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* 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.koin
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.app.job.JobScheduler
|
||||
import android.content.Context.JOB_SCHEDULER_SERVICE
|
||||
import android.content.Context.NOTIFICATION_SERVICE
|
||||
import androidx.room.Room.databaseBuilder
|
||||
import com.afollestad.nocknock.data.AppDatabase
|
||||
import com.afollestad.nocknock.data.Database1to2Migration
|
||||
import com.afollestad.nocknock.data.Database2to3Migration
|
||||
import com.afollestad.nocknock.data.Database3to4Migration
|
||||
import com.afollestad.nocknock.data.Database4to5Migration
|
||||
import com.afollestad.nocknock.notifications.Qualifiers.MAIN_ACTIVITY_CLASS
|
||||
import com.afollestad.nocknock.ui.main.MainActivity
|
||||
import com.afollestad.nocknock.utilities.ext.systemService
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koin.dsl.module.module
|
||||
|
||||
val mainActivityCls = MainActivity::class.java
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
val mainModule = module {
|
||||
|
||||
single(name = MAIN_ACTIVITY_CLASS) { mainActivityCls }
|
||||
|
||||
single {
|
||||
databaseBuilder(get(), AppDatabase::class.java, "NockNock.db")
|
||||
.addMigrations(
|
||||
Database1to2Migration(),
|
||||
Database2to3Migration(),
|
||||
Database3to4Migration(),
|
||||
Database4to5Migration()
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
single {
|
||||
OkHttpClient.Builder()
|
||||
.addNetworkInterceptor { chain ->
|
||||
val request = chain.request()
|
||||
.newBuilder()
|
||||
.addHeader("User-Agent", "com.afollestad.nocknock")
|
||||
.build()
|
||||
chain.proceed(request)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
single<JobScheduler> {
|
||||
get<Application>().systemService(JOB_SCHEDULER_SERVICE)
|
||||
}
|
||||
|
||||
single<NotificationManager> {
|
||||
get<Application>().systemService(NOTIFICATION_SERVICE)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* 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.koin
|
||||
|
||||
import com.afollestad.rxkprefs.RxkPrefs
|
||||
import com.afollestad.rxkprefs.rxkPrefs
|
||||
import org.koin.dsl.module.module
|
||||
|
||||
const val PREF_DARK_MODE = "dark_mode"
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
val prefModule = module {
|
||||
|
||||
single { rxkPrefs(get(), "settings") }
|
||||
|
||||
factory(name = PREF_DARK_MODE) {
|
||||
get<RxkPrefs>().boolean(PREF_DARK_MODE, false)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* 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.koin
|
||||
|
||||
import com.afollestad.nocknock.ui.addsite.AddSiteViewModel
|
||||
import com.afollestad.nocknock.ui.main.MainViewModel
|
||||
import com.afollestad.nocknock.ui.viewsite.ViewSiteViewModel
|
||||
import com.afollestad.nocknock.utilities.Qualifiers.IO_DISPATCHER
|
||||
import com.afollestad.nocknock.utilities.Qualifiers.MAIN_DISPATCHER
|
||||
import org.koin.androidx.viewmodel.ext.koin.viewModel
|
||||
import org.koin.dsl.module.module
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
val viewModelModule = module {
|
||||
|
||||
viewModel {
|
||||
MainViewModel(
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(name = MAIN_DISPATCHER),
|
||||
get(name = IO_DISPATCHER)
|
||||
)
|
||||
}
|
||||
|
||||
viewModel {
|
||||
AddSiteViewModel(
|
||||
get(),
|
||||
get(),
|
||||
get(name = MAIN_DISPATCHER),
|
||||
get(name = IO_DISPATCHER)
|
||||
)
|
||||
}
|
||||
|
||||
viewModel {
|
||||
ViewSiteViewModel(
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(name = MAIN_DISPATCHER),
|
||||
get(name = IO_DISPATCHER)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* 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.logging
|
||||
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import timber.log.Timber
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class FabricTree : Timber.Tree() {
|
||||
|
||||
override fun log(
|
||||
priority: Int,
|
||||
tag: String?,
|
||||
message: String,
|
||||
t: Throwable?
|
||||
) {
|
||||
if (t != null) {
|
||||
Crashlytics.setString("crash_tag", tag)
|
||||
Crashlytics.logException(t)
|
||||
} else {
|
||||
Crashlytics.log(priority, tag, message)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* 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
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.koin.PREF_DARK_MODE
|
||||
import com.afollestad.nocknock.ui.NightMode.DISABLED
|
||||
import com.afollestad.nocknock.ui.NightMode.ENABLED
|
||||
import com.afollestad.nocknock.ui.NightMode.UNKNOWN
|
||||
import com.afollestad.nocknock.utilities.rx.attachLifecycle
|
||||
import com.afollestad.rxkprefs.Pref
|
||||
import org.koin.android.ext.android.inject
|
||||
import timber.log.Timber.d as log
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
abstract class DarkModeSwitchActivity : AppCompatActivity() {
|
||||
|
||||
private var isDarkModeEnabled: Boolean = false
|
||||
private val darkModePref by inject<Pref<Boolean>>(name = PREF_DARK_MODE)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
isDarkModeEnabled = isDarkMode()
|
||||
setTheme(themeRes())
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (getCurrentNightMode() == UNKNOWN) {
|
||||
darkModePref.observe()
|
||||
.filter { it != isDarkModeEnabled }
|
||||
.subscribe {
|
||||
log("Theme changed, recreating Activity.")
|
||||
recreate()
|
||||
}
|
||||
.attachLifecycle(this)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun getCurrentNightMode(): NightMode {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
return UNKNOWN
|
||||
}
|
||||
return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
|
||||
Configuration.UI_MODE_NIGHT_YES -> return ENABLED
|
||||
Configuration.UI_MODE_NIGHT_NO -> return DISABLED
|
||||
else -> UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
protected fun isDarkMode(): Boolean {
|
||||
return when (getCurrentNightMode()) {
|
||||
ENABLED -> true
|
||||
DISABLED -> false
|
||||
else -> darkModePref.get()
|
||||
}
|
||||
}
|
||||
|
||||
protected fun toggleDarkMode() = setDarkMode(!isDarkMode())
|
||||
|
||||
private fun setDarkMode(darkMode: Boolean) = darkModePref.set(darkMode)
|
||||
|
||||
private fun themeRes() = if (isDarkMode()) {
|
||||
R.style.AppTheme_Dark
|
||||
} else {
|
||||
R.style.AppTheme
|
||||
}
|
||||
}
|
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
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* 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
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
abstract class ScopedViewModel(mainDispatcher: CoroutineDispatcher) : ViewModel() {
|
||||
|
||||
private val job = Job()
|
||||
protected val scope = CoroutineScope(job + mainDispatcher)
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@TestOnly open fun destroy() = job.cancel()
|
||||
}
|
|
@ -1,88 +1,137 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.addsite
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.Intent.ACTION_OPEN_DOCUMENT
|
||||
import android.content.Intent.CATEGORY_OPENABLE
|
||||
import android.os.Bundle
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ValidationMode
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.indexToValidationMode
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
|
||||
import com.afollestad.nocknock.viewcomponents.ext.conceal
|
||||
import com.afollestad.nocknock.viewcomponents.ext.onItemSelected
|
||||
import com.afollestad.nocknock.viewcomponents.ext.onLayout
|
||||
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
|
||||
import com.afollestad.nocknock.viewcomponents.ext.trimmedText
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.ValidationMode
|
||||
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
||||
import com.afollestad.nocknock.ui.viewsite.KEY_SITE
|
||||
import com.afollestad.nocknock.utilities.ext.onTextChanged
|
||||
import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
|
||||
import com.afollestad.nocknock.utilities.livedata.distinct
|
||||
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
|
||||
import com.afollestad.nocknock.viewcomponents.ext.isVisibleCondition
|
||||
import com.afollestad.nocknock.viewcomponents.ext.onScroll
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||
import com.afollestad.vvalidator.form
|
||||
import com.afollestad.vvalidator.form.Form
|
||||
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout
|
||||
import kotlinx.android.synthetic.main.activity_addsite.doneBtn
|
||||
import kotlinx.android.synthetic.main.activity_addsite.headersLayout
|
||||
import kotlinx.android.synthetic.main.activity_addsite.inputName
|
||||
import kotlinx.android.synthetic.main.activity_addsite.inputTags
|
||||
import kotlinx.android.synthetic.main.activity_addsite.inputUrl
|
||||
import kotlinx.android.synthetic.main.activity_addsite.loadingProgress
|
||||
import kotlinx.android.synthetic.main.activity_addsite.responseTimeoutInput
|
||||
import kotlinx.android.synthetic.main.activity_addsite.responseValidationMode
|
||||
import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchTerm
|
||||
import kotlinx.android.synthetic.main.activity_addsite.rootView
|
||||
import kotlinx.android.synthetic.main.activity_addsite.retryPolicyLayout
|
||||
import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout
|
||||
import kotlinx.android.synthetic.main.activity_addsite.scrollView
|
||||
import kotlinx.android.synthetic.main.activity_addsite.sslCertificateBrowse
|
||||
import kotlinx.android.synthetic.main.activity_addsite.sslCertificateInput
|
||||
import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning
|
||||
import kotlinx.android.synthetic.main.activity_addsite.toolbar
|
||||
import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.math.max
|
||||
import kotlin.properties.Delegates.notNull
|
||||
|
||||
const val KEY_FAB_X = "fab_x"
|
||||
const val KEY_FAB_Y = "fab_y"
|
||||
const val KEY_FAB_SIZE = "fab_size"
|
||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import kotlinx.android.synthetic.main.include_app_bar.app_toolbar as appToolbar
|
||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class AddSiteActivity : AppCompatActivity(), AddSiteView {
|
||||
class AddSiteActivity : DarkModeSwitchActivity() {
|
||||
companion object {
|
||||
private const val SELECT_CERT_FILE_RQ = 23
|
||||
}
|
||||
|
||||
var isClosing: Boolean = false
|
||||
var revealCx by notNull<Int>()
|
||||
var revealCy by notNull<Int>()
|
||||
var revealRadius by notNull<Float>()
|
||||
|
||||
@Inject lateinit var presenter: AddSitePresenter
|
||||
private val viewModel by viewModel<AddSiteViewModel>()
|
||||
private lateinit var validationForm: Form
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
injector().injectInto(this)
|
||||
setContentView(R.layout.activity_addsite)
|
||||
presenter.takeView(this)
|
||||
setupUi()
|
||||
setupValidation()
|
||||
|
||||
toolbar.setNavigationOnClickListener { closeActivityWithReveal() }
|
||||
lifecycle.addObserver(viewModel)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
rootView.conceal()
|
||||
rootView.onLayout {
|
||||
val fabSize = intent.getIntExtra(KEY_FAB_SIZE, 0)
|
||||
val fabX = intent.getFloatExtra(KEY_FAB_X, 0f)
|
||||
.toInt()
|
||||
val fabY = intent.getFloatExtra(KEY_FAB_Y, 0f)
|
||||
.toInt()
|
||||
// Populate view model with initial data
|
||||
val model = intent.getSerializableExtra(KEY_SITE) as? Site
|
||||
model?.let { viewModel.prePopulateFromModel(model) }
|
||||
|
||||
revealCx = fabX + fabSize / 2
|
||||
revealCy = (fabY + toolbar.measuredHeight + fabSize / 2)
|
||||
revealRadius = max(revealCx, revealCy).toFloat()
|
||||
// Loading
|
||||
loadingProgress.observe(this, viewModel.onIsLoading())
|
||||
|
||||
circularRevealActivity()
|
||||
}
|
||||
}
|
||||
// Name
|
||||
inputName.attachLiveData(this, viewModel.name)
|
||||
|
||||
inputUrl.setOnFocusChangeListener { _, hasFocus ->
|
||||
presenter.onUrlInputFocusChange(hasFocus, inputUrl.trimmedText())
|
||||
// Tags
|
||||
inputTags.attachLiveData(this, viewModel.tags)
|
||||
|
||||
// Url
|
||||
inputUrl.attachLiveData(this, viewModel.url)
|
||||
viewModel.onUrlWarningVisibility()
|
||||
.toViewVisibility(this, textUrlWarning)
|
||||
|
||||
// Timeout
|
||||
responseTimeoutInput.attachLiveData(this, viewModel.timeout)
|
||||
|
||||
// Validation mode
|
||||
responseValidationMode.attachLiveData(
|
||||
lifecycleOwner = this,
|
||||
data = viewModel.validationMode,
|
||||
outTransformer = { ValidationMode.fromIndex(it) },
|
||||
inTransformer = { it.toIndex() }
|
||||
)
|
||||
viewModel.onValidationModeDescription()
|
||||
.toViewText(this, validationModeDescription)
|
||||
|
||||
// Validation search term
|
||||
responseValidationSearchTerm.attachLiveData(
|
||||
lifecycleOwner = this,
|
||||
data = viewModel.validationSearchTerm,
|
||||
pullInChanges = false
|
||||
)
|
||||
viewModel.onValidationSearchTermVisibility()
|
||||
.toViewVisibility(this, responseValidationSearchTerm)
|
||||
|
||||
// SSL certificate
|
||||
sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it }
|
||||
viewModel.certificateUri.distinct()
|
||||
.observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) })
|
||||
|
||||
// Headers
|
||||
headersLayout.attach(viewModel.headers)
|
||||
}
|
||||
|
||||
private fun setupUi() {
|
||||
toolbarTitle.setText(R.string.add_site)
|
||||
toolbar.run {
|
||||
inflateMenu(R.menu.menu_addsite)
|
||||
setNavigationIcon(R.drawable.ic_action_close)
|
||||
setNavigationOnClickListener { finish() }
|
||||
}
|
||||
|
||||
val validationOptionsAdapter = ArrayAdapter(
|
||||
|
@ -91,98 +140,96 @@ class AddSiteActivity : AppCompatActivity(), AddSiteView {
|
|||
resources.getStringArray(R.array.response_validation_options)
|
||||
)
|
||||
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
||||
|
||||
responseValidationMode.adapter = validationOptionsAdapter
|
||||
responseValidationMode.onItemSelected(presenter::onValidationModeSelected)
|
||||
|
||||
doneBtn.setOnClickListener {
|
||||
val checkInterval = checkIntervalLayout.getSelectedCheckInterval()
|
||||
val validationMode =
|
||||
responseValidationMode.selectedItemPosition.indexToValidationMode()
|
||||
scrollView.onScroll {
|
||||
appToolbar.elevation = if (it > appToolbar.measuredHeight / 2) {
|
||||
appToolbar.dimenFloat(R.dimen.default_elevation)
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
|
||||
isClosing = true
|
||||
presenter.commit(
|
||||
name = inputName.trimmedText(),
|
||||
url = inputUrl.trimmedText(),
|
||||
checkInterval = checkInterval,
|
||||
validationMode = validationMode,
|
||||
validationContent = validationMode.validationContent()
|
||||
)
|
||||
// SSL certificate
|
||||
sslCertificateBrowse.setOnClickListener {
|
||||
val intent = Intent(ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
}
|
||||
startActivityForResult(intent, SELECT_CERT_FILE_RQ)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
presenter.dropView()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun setLoading() = loadingProgress.setLoading()
|
||||
|
||||
override fun setDoneLoading() = loadingProgress.setDone()
|
||||
|
||||
override fun showOrHideUrlSchemeWarning(show: Boolean) {
|
||||
textUrlWarning.showOrHide(show)
|
||||
if (show) {
|
||||
textUrlWarning.setText(R.string.warning_http_url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showOrHideValidationSearchTerm(show: Boolean) =
|
||||
responseValidationSearchTerm.showOrHide(show)
|
||||
|
||||
override fun showOrHideScriptInput(show: Boolean) = scriptInputLayout.showOrHide(show)
|
||||
|
||||
override fun setValidationModeDescription(res: Int) = validationModeDescription.setText(res)
|
||||
|
||||
override fun setInputErrors(errors: InputErrors) {
|
||||
isClosing = false
|
||||
inputName.error = if (errors.name != null) {
|
||||
getString(errors.name!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
inputUrl.error = if (errors.url != null) {
|
||||
getString(errors.url!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
checkIntervalLayout.setError(
|
||||
if (errors.checkInterval != null) {
|
||||
getString(errors.checkInterval!!)
|
||||
} else {
|
||||
null
|
||||
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
|
||||
)
|
||||
responseValidationSearchTerm.error = if (errors.termSearch != null) {
|
||||
getString(errors.termSearch!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
scriptInputLayout.setError(
|
||||
if (errors.javaScript != null) {
|
||||
getString(errors.javaScript!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// 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 onSiteAdded() {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
overridePendingTransition(R.anim.fade_out, R.anim.fade_out)
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {
|
||||
appToolbar.dimenFloat(R.dimen.default_elevation)
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
|
||||
override fun scopeWhileAttached(
|
||||
context: CoroutineContext,
|
||||
exec: ScopeReceiver
|
||||
) = rootView.scopeWhileAttached(context, exec)
|
||||
|
||||
override fun onBackPressed() = closeActivityWithReveal()
|
||||
|
||||
private fun ValidationMode.validationContent() = when (this) {
|
||||
STATUS_CODE -> null
|
||||
TERM_SEARCH -> responseValidationSearchTerm.trimmedText()
|
||||
JAVASCRIPT -> scriptInputLayout.getCode()
|
||||
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() ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.addsite
|
||||
|
||||
import android.view.ViewAnimationUtils
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import com.afollestad.nocknock.utilities.ext.onEnd
|
||||
import com.afollestad.nocknock.viewcomponents.ext.conceal
|
||||
import com.afollestad.nocknock.viewcomponents.ext.show
|
||||
import kotlinx.android.synthetic.main.activity_addsite.rootView
|
||||
|
||||
const val REVEAL_DURATION = 300L
|
||||
|
||||
internal fun AddSiteActivity.circularRevealActivity() {
|
||||
val circularReveal =
|
||||
ViewAnimationUtils.createCircularReveal(rootView, revealCx, revealCy, 0f, revealRadius)
|
||||
.apply {
|
||||
duration = REVEAL_DURATION
|
||||
interpolator = DecelerateInterpolator()
|
||||
}
|
||||
rootView.show()
|
||||
circularReveal.start()
|
||||
}
|
||||
|
||||
internal fun AddSiteActivity.closeActivityWithReveal() {
|
||||
if (isClosing) return
|
||||
isClosing = true
|
||||
ViewAnimationUtils.createCircularReveal(rootView, revealCx, revealCy, revealRadius, 0f)
|
||||
.apply {
|
||||
duration = REVEAL_DURATION
|
||||
interpolator = AccelerateInterpolator()
|
||||
onEnd {
|
||||
rootView.conceal()
|
||||
finish()
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
start()
|
||||
}
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.addsite
|
||||
|
||||
import androidx.annotation.CheckResult
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
||||
import com.afollestad.nocknock.data.ValidationMode
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.HttpUrl
|
||||
import javax.inject.Inject
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
data class InputErrors(
|
||||
var name: Int? = null,
|
||||
var url: Int? = null,
|
||||
var checkInterval: Int? = null,
|
||||
var termSearch: Int? = null,
|
||||
var javaScript: Int? = null
|
||||
) {
|
||||
@CheckResult fun any(): Boolean {
|
||||
return name != null || url != null || checkInterval != null ||
|
||||
termSearch != null || javaScript != null
|
||||
}
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
interface AddSitePresenter {
|
||||
|
||||
fun takeView(view: AddSiteView)
|
||||
|
||||
fun onUrlInputFocusChange(
|
||||
focused: Boolean,
|
||||
content: String
|
||||
)
|
||||
|
||||
fun onValidationModeSelected(index: Int)
|
||||
|
||||
fun commit(
|
||||
name: String,
|
||||
url: String,
|
||||
checkInterval: Long,
|
||||
validationMode: ValidationMode,
|
||||
validationContent: String?
|
||||
)
|
||||
|
||||
fun dropView()
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class RealAddSitePresenter @Inject constructor(
|
||||
private val serverModelStore: ServerModelStore,
|
||||
private val checkStatusManager: CheckStatusManager
|
||||
) : AddSitePresenter {
|
||||
|
||||
private var view: AddSiteView? = null
|
||||
|
||||
override fun takeView(view: AddSiteView) {
|
||||
this.view = view
|
||||
}
|
||||
|
||||
override fun onUrlInputFocusChange(
|
||||
focused: Boolean,
|
||||
content: String
|
||||
) {
|
||||
if (content.isEmpty() || focused) {
|
||||
return
|
||||
}
|
||||
val url = HttpUrl.parse(content)
|
||||
if (url == null ||
|
||||
(url.scheme() != "http" &&
|
||||
url.scheme() != "https")
|
||||
) {
|
||||
view?.showOrHideUrlSchemeWarning(true)
|
||||
} else {
|
||||
view?.showOrHideUrlSchemeWarning(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onValidationModeSelected(index: Int) = with(view!!) {
|
||||
showOrHideValidationSearchTerm(index == 1)
|
||||
showOrHideScriptInput(index == 2)
|
||||
setValidationModeDescription(
|
||||
when (index) {
|
||||
0 -> R.string.validation_mode_status_desc
|
||||
1 -> R.string.validation_mode_term_desc
|
||||
2 -> R.string.validation_mode_javascript_desc
|
||||
else -> throw IllegalStateException("Unknown validation mode position: $index")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun commit(
|
||||
name: String,
|
||||
url: String,
|
||||
checkInterval: Long,
|
||||
validationMode: ValidationMode,
|
||||
validationContent: String?
|
||||
) {
|
||||
val inputErrors = InputErrors()
|
||||
|
||||
if (name.isEmpty()) {
|
||||
inputErrors.name = R.string.please_enter_name
|
||||
}
|
||||
if (url.isEmpty()) {
|
||||
inputErrors.url = R.string.please_enter_url
|
||||
} else if (HttpUrl.parse(url) == null) {
|
||||
inputErrors.url = R.string.please_enter_valid_url
|
||||
}
|
||||
if (checkInterval <= 0) {
|
||||
inputErrors.checkInterval = R.string.please_enter_check_interval
|
||||
}
|
||||
if (validationMode == TERM_SEARCH && validationContent.isNullOrEmpty()) {
|
||||
inputErrors.termSearch = R.string.please_enter_search_term
|
||||
} else if (validationMode == JAVASCRIPT && validationContent.isNullOrEmpty()) {
|
||||
inputErrors.javaScript = R.string.please_enter_javaScript
|
||||
}
|
||||
|
||||
if (inputErrors.any()) {
|
||||
view?.setInputErrors(inputErrors)
|
||||
return
|
||||
}
|
||||
|
||||
val newModel = ServerModel(
|
||||
name = name,
|
||||
url = url,
|
||||
status = WAITING,
|
||||
checkInterval = checkInterval,
|
||||
validationMode = validationMode,
|
||||
validationContent = validationContent
|
||||
)
|
||||
|
||||
with(view!!) {
|
||||
scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
setLoading()
|
||||
val storedModel = async(IO) {
|
||||
serverModelStore.put(newModel)
|
||||
}.await()
|
||||
|
||||
checkStatusManager.scheduleCheck(
|
||||
site = storedModel,
|
||||
rightNow = true,
|
||||
cancelPrevious = true
|
||||
)
|
||||
setDoneLoading()
|
||||
onSiteAdded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dropView() {
|
||||
view = null
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.addsite
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
interface AddSiteView {
|
||||
|
||||
fun setLoading()
|
||||
|
||||
fun setDoneLoading()
|
||||
|
||||
fun showOrHideUrlSchemeWarning(show: Boolean)
|
||||
|
||||
fun showOrHideValidationSearchTerm(show: Boolean)
|
||||
|
||||
fun showOrHideScriptInput(show: Boolean)
|
||||
|
||||
fun setValidationModeDescription(@StringRes res: Int)
|
||||
|
||||
fun setInputErrors(errors: InputErrors)
|
||||
|
||||
fun onSiteAdded()
|
||||
|
||||
fun scopeWhileAttached(
|
||||
context: CoroutineContext,
|
||||
exec: ScopeReceiver
|
||||
)
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
/**
|
||||
* 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 androidx.annotation.CheckResult
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.VisibleForTesting.PRIVATE
|
||||
import androidx.lifecycle.Lifecycle.Event.ON_START
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.AppDatabase
|
||||
import com.afollestad.nocknock.data.model.Header
|
||||
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.SiteSettings
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
import com.afollestad.nocknock.data.model.ValidationMode
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.model.ValidationResult
|
||||
import com.afollestad.nocknock.data.putSite
|
||||
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||
import com.afollestad.nocknock.ui.ScopedViewModel
|
||||
import com.afollestad.nocknock.utilities.ext.MINUTE
|
||||
import com.afollestad.nocknock.utilities.livedata.map
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.HttpUrl
|
||||
import java.lang.System.currentTimeMillis
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class AddSiteViewModel(
|
||||
private val database: AppDatabase,
|
||||
private val validationManager: ValidationExecutor,
|
||||
mainDispatcher: CoroutineDispatcher,
|
||||
private val ioDispatcher: CoroutineDispatcher
|
||||
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
|
||||
|
||||
// Public properties
|
||||
val name = MutableLiveData<String>()
|
||||
val tags = MutableLiveData<String>()
|
||||
val url = MutableLiveData<String>()
|
||||
val timeout = MutableLiveData<Int>()
|
||||
val validationMode = MutableLiveData<ValidationMode>()
|
||||
val validationSearchTerm = MutableLiveData<String>()
|
||||
val validationScript = MutableLiveData<String>()
|
||||
val checkIntervalValue = MutableLiveData<Int>()
|
||||
val checkIntervalUnit = MutableLiveData<Long>()
|
||||
val retryPolicyTimes = MutableLiveData<Int>()
|
||||
val retryPolicyMinutes = MutableLiveData<Int>()
|
||||
val headers = MutableLiveData<List<Header>>()
|
||||
val certificateUri = MutableLiveData<String>()
|
||||
|
||||
@OnLifecycleEvent(ON_START)
|
||||
fun setDefaults() {
|
||||
timeout.value = 10000
|
||||
validationMode.value = STATUS_CODE
|
||||
checkIntervalValue.value = 0
|
||||
checkIntervalUnit.value = MINUTE
|
||||
retryPolicyMinutes.value = 0
|
||||
retryPolicyMinutes.value = 0
|
||||
tags.value = ""
|
||||
headers.value = emptyList()
|
||||
}
|
||||
|
||||
private val isLoading = MutableLiveData<Boolean>()
|
||||
|
||||
@CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading
|
||||
|
||||
@CheckResult fun onUrlWarningVisibility(): LiveData<Boolean> {
|
||||
return url.map {
|
||||
val parsed = HttpUrl.parse(it)
|
||||
return@map it.isNotEmpty() && parsed == null
|
||||
}
|
||||
}
|
||||
|
||||
@CheckResult fun onValidationModeDescription(): LiveData<Int> {
|
||||
return validationMode.map {
|
||||
when (it!!) {
|
||||
STATUS_CODE -> R.string.validation_mode_status_desc
|
||||
TERM_SEARCH -> R.string.validation_mode_term_desc
|
||||
JAVASCRIPT -> R.string.validation_mode_javascript_desc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CheckResult fun onValidationSearchTermVisibility() = validationMode.map { it == TERM_SEARCH }
|
||||
|
||||
@CheckResult fun onValidationScriptVisibility() = validationMode.map { it == JAVASCRIPT }
|
||||
|
||||
// Actions
|
||||
fun commit(done: () -> Unit) {
|
||||
scope.launch {
|
||||
val newModel = generateDbModel() ?: return@launch
|
||||
isLoading.value = true
|
||||
|
||||
val storedModel = withContext(ioDispatcher) {
|
||||
database.putSite(newModel)
|
||||
}
|
||||
validationManager.scheduleValidation(
|
||||
site = storedModel,
|
||||
rightNow = true,
|
||||
cancelPrevious = true
|
||||
)
|
||||
|
||||
isLoading.value = false
|
||||
done()
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
@VisibleForTesting(otherwise = PRIVATE)
|
||||
fun getCheckIntervalMs(): Long {
|
||||
val value = checkIntervalValue.value ?: return 0
|
||||
val unit = checkIntervalUnit.value ?: return 0
|
||||
return value * unit
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = PRIVATE)
|
||||
fun getValidationArgs(): String? {
|
||||
return when (validationMode.value) {
|
||||
TERM_SEARCH -> validationSearchTerm.value
|
||||
JAVASCRIPT -> validationScript.value
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateDbModel(): Site? {
|
||||
val timeout = timeout.value ?: 10_000
|
||||
val cleanedTags = tags.value?.split(',')?.joinToString { it.trim() } ?: ""
|
||||
|
||||
val newSettings = SiteSettings(
|
||||
validationIntervalMs = getCheckIntervalMs(),
|
||||
validationMode = validationMode.value!!,
|
||||
validationArgs = getValidationArgs(),
|
||||
networkTimeout = timeout,
|
||||
disabled = false,
|
||||
certificate = certificateUri.value?.toString()
|
||||
)
|
||||
|
||||
val newLastResult = ValidationResult(
|
||||
timestampMs = currentTimeMillis(),
|
||||
status = WAITING,
|
||||
reason = null
|
||||
)
|
||||
|
||||
val retryPolicyTimes = retryPolicyTimes.value ?: 0
|
||||
val retryPolicyMinutes = retryPolicyMinutes.value ?: 0
|
||||
val newRetryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) {
|
||||
RetryPolicy(
|
||||
count = retryPolicyTimes,
|
||||
minutes = retryPolicyMinutes
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return Site(
|
||||
id = 0,
|
||||
name = name.value!!.trim(),
|
||||
url = url.value!!.trim(),
|
||||
tags = cleanedTags,
|
||||
settings = newSettings,
|
||||
lastResult = newLastResult,
|
||||
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()
|
||||
}
|
|
@ -1,129 +1,134 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.main
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.list.listItems
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.adapter.ServerAdapter
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.adapter.SiteAdapter
|
||||
import com.afollestad.nocknock.adapter.TagAdapter
|
||||
import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.dialogs.AboutDialog
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
import com.afollestad.nocknock.utilities.ext.safeRegisterReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.safeUnregisterReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
|
||||
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
||||
import com.afollestad.nocknock.ui.NightMode.UNKNOWN
|
||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||
import kotlinx.android.synthetic.main.activity_main.fab
|
||||
import kotlinx.android.synthetic.main.activity_main.list
|
||||
import kotlinx.android.synthetic.main.activity_main.rootView
|
||||
import kotlinx.android.synthetic.main.activity_main.toolbar
|
||||
import kotlinx.android.synthetic.main.activity_main.loadingProgress
|
||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||
import kotlinx.android.synthetic.main.include_empty_view.emptyText
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import kotlinx.android.synthetic.main.activity_main.tags_list as tagsList
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class MainActivity : AppCompatActivity(), MainView {
|
||||
class MainActivity : DarkModeSwitchActivity() {
|
||||
|
||||
private val intentReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
intent: Intent
|
||||
) = presenter.onBroadcast(intent)
|
||||
private val notificationManager by inject<NockNotificationManager>()
|
||||
private val intentProvider by inject<IntentProvider>()
|
||||
|
||||
internal val viewModel by viewModel<MainViewModel>()
|
||||
|
||||
private lateinit var siteAdapter: SiteAdapter
|
||||
private lateinit var tagAdapter: TagAdapter
|
||||
|
||||
private val statusUpdateReceiver by lazy {
|
||||
StatusUpdateIntentReceiver(application, intentProvider) {
|
||||
viewModel.postSiteUpdate(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Inject lateinit var presenter: MainPresenter
|
||||
|
||||
private lateinit var adapter: ServerAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
injector().injectInto(this)
|
||||
setContentView(R.layout.activity_main)
|
||||
presenter.takeView(this)
|
||||
setupUi()
|
||||
|
||||
toolbar.inflateMenu(R.menu.menu_main)
|
||||
toolbar.setOnMenuItemClickListener { item ->
|
||||
if (item.itemId == R.id.about) {
|
||||
AboutDialog.show(this)
|
||||
}
|
||||
return@setOnMenuItemClickListener true
|
||||
notificationManager.createChannels()
|
||||
|
||||
lifecycle.run {
|
||||
addObserver(viewModel)
|
||||
addObserver(statusUpdateReceiver)
|
||||
}
|
||||
|
||||
adapter = ServerAdapter(this::onSiteSelected)
|
||||
|
||||
list.layoutManager = LinearLayoutManager(this)
|
||||
list.adapter = adapter
|
||||
list.addItemDecoration(DividerItemDecoration(this, VERTICAL))
|
||||
|
||||
fab.setOnClickListener { addSite() }
|
||||
viewModel.onSites()
|
||||
.observe(this, Observer { siteAdapter.set(it) })
|
||||
viewModel.onEmptyTextVisibility()
|
||||
.toViewVisibility(this, emptyText)
|
||||
viewModel.onTags()
|
||||
.observe(this, Observer { tagAdapter.set(it) })
|
||||
viewModel.onTagsListVisibility()
|
||||
.toViewVisibility(this, tagsList)
|
||||
loadingProgress.observe(this, viewModel.onIsLoading())
|
||||
|
||||
processIntent(intent)
|
||||
}
|
||||
|
||||
private fun setupUi() {
|
||||
toolbar.run {
|
||||
inflateMenu(R.menu.menu_main)
|
||||
menu.findItem(R.id.dark_mode)
|
||||
.apply {
|
||||
if (getCurrentNightMode() == UNKNOWN) {
|
||||
isChecked = isDarkMode()
|
||||
} else {
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.about -> AboutDialog.show(this@MainActivity)
|
||||
R.id.dark_mode -> toggleDarkMode()
|
||||
}
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
}
|
||||
|
||||
siteAdapter = SiteAdapter(this::onSiteSelected)
|
||||
list.run {
|
||||
layoutManager = LinearLayoutManager(this@MainActivity)
|
||||
adapter = siteAdapter
|
||||
addItemDecoration(DividerItemDecoration(this@MainActivity, VERTICAL))
|
||||
}
|
||||
|
||||
tagAdapter = TagAdapter(viewModel::onTagSelection)
|
||||
tagsList.run {
|
||||
layoutManager = LinearLayoutManager(this@MainActivity, HORIZONTAL, false)
|
||||
adapter = tagAdapter
|
||||
}
|
||||
|
||||
fab.setOnClickListener { addSite() }
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
intent?.let(::processIntent)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val filter = IntentFilter().apply {
|
||||
addAction(ACTION_STATUS_UPDATE)
|
||||
}
|
||||
safeRegisterReceiver(intentReceiver, filter)
|
||||
presenter.resume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
safeUnregisterReceiver(intentReceiver)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
presenter.dropView()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun setModels(models: List<ServerModel>) {
|
||||
list.post {
|
||||
adapter.set(models)
|
||||
emptyText.showOrHide(models.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateModel(model: ServerModel) {
|
||||
list.post { adapter.update(model) }
|
||||
}
|
||||
|
||||
override fun onSiteDeleted(model: ServerModel) {
|
||||
list.post {
|
||||
adapter.remove(model)
|
||||
emptyText.showOrHide(adapter.itemCount == 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun scopeWhileAttached(
|
||||
context: CoroutineContext,
|
||||
exec: ScopeReceiver
|
||||
) = rootView.scopeWhileAttached(context, exec)
|
||||
|
||||
private fun onSiteSelected(
|
||||
model: ServerModel,
|
||||
model: Site,
|
||||
longClick: Boolean
|
||||
) {
|
||||
if (longClick) {
|
||||
|
@ -131,8 +136,9 @@ class MainActivity : AppCompatActivity(), MainView {
|
|||
title(R.string.options)
|
||||
listItems(R.array.site_long_options) { _, i, _ ->
|
||||
when (i) {
|
||||
0 -> presenter.refreshSite(model)
|
||||
1 -> maybeRemoveSite(model)
|
||||
0 -> viewModel.refreshSite(model)
|
||||
1 -> addSiteForDuplication(model)
|
||||
2 -> maybeRemoveSite(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,63 +1,73 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.main
|
||||
|
||||
import android.content.Intent
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.toHtml
|
||||
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
||||
import com.afollestad.nocknock.ui.addsite.KEY_FAB_SIZE
|
||||
import com.afollestad.nocknock.ui.addsite.KEY_FAB_X
|
||||
import com.afollestad.nocknock.ui.addsite.KEY_FAB_Y
|
||||
import com.afollestad.nocknock.ui.viewsite.KEY_VIEW_MODEL
|
||||
import com.afollestad.nocknock.ui.viewsite.KEY_SITE
|
||||
import com.afollestad.nocknock.ui.viewsite.ViewSiteActivity
|
||||
import com.afollestad.nocknock.utilities.providers.RealIntentProvider.Companion.KEY_VIEW_NOTIFICATION_MODEL
|
||||
import kotlinx.android.synthetic.main.activity_main.fab
|
||||
|
||||
internal const val VIEW_SITE_RQ = 6923
|
||||
internal const val ADD_SITE_RQ = 6969
|
||||
|
||||
// ADD
|
||||
|
||||
internal fun MainActivity.addSite() {
|
||||
startActivityForResult(intentToAdd(fab.x, fab.y, fab.measuredWidth), ADD_SITE_RQ)
|
||||
startActivityForResult(intentToAdd(), ADD_SITE_RQ)
|
||||
}
|
||||
|
||||
private fun MainActivity.intentToAdd(
|
||||
x: Float,
|
||||
y: Float,
|
||||
size: Int
|
||||
) = Intent(this, AddSiteActivity::class.java).apply {
|
||||
putExtra(KEY_FAB_X, x)
|
||||
putExtra(KEY_FAB_Y, y)
|
||||
putExtra(KEY_FAB_SIZE, size)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||
internal fun MainActivity.addSiteForDuplication(site: Site) {
|
||||
startActivityForResult(intentToAdd(site), ADD_SITE_RQ)
|
||||
}
|
||||
|
||||
internal fun MainActivity.viewSite(model: ServerModel) {
|
||||
private fun MainActivity.intentToAdd(model: Site? = null) =
|
||||
Intent(this, AddSiteActivity::class.java).apply {
|
||||
model?.let { putExtra(KEY_SITE, it) }
|
||||
}
|
||||
|
||||
// VIEW
|
||||
|
||||
internal fun MainActivity.viewSite(model: Site) {
|
||||
startActivityForResult(intentToView(model), VIEW_SITE_RQ)
|
||||
}
|
||||
|
||||
private fun MainActivity.intentToView(model: ServerModel) =
|
||||
private fun MainActivity.intentToView(model: Site) =
|
||||
Intent(this, ViewSiteActivity::class.java).apply {
|
||||
putExtra(KEY_VIEW_MODEL, model)
|
||||
putExtra(KEY_SITE, model)
|
||||
}
|
||||
|
||||
internal fun MainActivity.maybeRemoveSite(model: ServerModel) {
|
||||
// MISC
|
||||
|
||||
internal fun MainActivity.maybeRemoveSite(model: Site) {
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.remove_site)
|
||||
message(text = context.getString(R.string.remove_site_prompt, model.name).toHtml())
|
||||
positiveButton(R.string.remove) { presenter.removeSite(model) }
|
||||
positiveButton(R.string.remove) { viewModel.removeSite(model) }
|
||||
negativeButton(android.R.string.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun MainActivity.processIntent(intent: Intent) {
|
||||
if (intent.hasExtra(KEY_VIEW_NOTIFICATION_MODEL)) {
|
||||
val model = intent.getSerializableExtra(KEY_VIEW_NOTIFICATION_MODEL) as ServerModel
|
||||
val model = intent.getSerializableExtra(KEY_VIEW_NOTIFICATION_MODEL) as Site
|
||||
viewSite(model)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.main
|
||||
|
||||
import android.content.Intent
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
interface MainPresenter {
|
||||
|
||||
fun takeView(view: MainView)
|
||||
|
||||
fun onBroadcast(intent: Intent)
|
||||
|
||||
fun resume()
|
||||
|
||||
fun refreshSite(site: ServerModel)
|
||||
|
||||
fun removeSite(site: ServerModel)
|
||||
|
||||
fun dropView()
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class RealMainPresenter @Inject constructor(
|
||||
private val serverModelStore: ServerModelStore,
|
||||
private val notificationManager: NockNotificationManager,
|
||||
private val checkStatusManager: CheckStatusManager
|
||||
) : MainPresenter {
|
||||
|
||||
private var view: MainView? = null
|
||||
|
||||
override fun takeView(view: MainView) {
|
||||
this.view = view
|
||||
notificationManager.createChannels()
|
||||
ensureCheckJobs()
|
||||
}
|
||||
|
||||
override fun onBroadcast(intent: Intent) {
|
||||
if (intent.action == ACTION_STATUS_UPDATE) {
|
||||
val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? ServerModel ?: return
|
||||
view?.updateModel(model)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resume() {
|
||||
notificationManager.cancelStatusNotifications()
|
||||
view!!.run {
|
||||
setModels(listOf())
|
||||
scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
val models = async(IO) {
|
||||
serverModelStore.get()
|
||||
}.await()
|
||||
setModels(models)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun refreshSite(site: ServerModel) {
|
||||
checkStatusManager.scheduleCheck(
|
||||
site = site,
|
||||
rightNow = true,
|
||||
cancelPrevious = true
|
||||
)
|
||||
}
|
||||
|
||||
override fun removeSite(site: ServerModel) {
|
||||
checkStatusManager.cancelCheck(site)
|
||||
notificationManager.cancelStatusNotification(site)
|
||||
view!!.scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
async(IO) { serverModelStore.delete(site) }.await()
|
||||
view?.onSiteDeleted(site)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dropView() {
|
||||
view = null
|
||||
}
|
||||
|
||||
private fun ensureCheckJobs() {
|
||||
view!!.scopeWhileAttached(IO) {
|
||||
launch(coroutineContext) {
|
||||
checkStatusManager.ensureScheduledChecks()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.main
|
||||
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
interface MainView {
|
||||
|
||||
fun setModels(models: List<ServerModel>)
|
||||
|
||||
fun updateModel(model: ServerModel)
|
||||
|
||||
fun onSiteDeleted(model: ServerModel)
|
||||
|
||||
fun scopeWhileAttached(
|
||||
context: CoroutineContext,
|
||||
exec: ScopeReceiver
|
||||
)
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
/**
|
||||
* 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.main
|
||||
|
||||
import androidx.annotation.CheckResult
|
||||
import androidx.lifecycle.Lifecycle.Event.ON_RESUME
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import com.afollestad.nocknock.data.AppDatabase
|
||||
import com.afollestad.nocknock.data.allSites
|
||||
import com.afollestad.nocknock.data.deleteSite
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.ui.ScopedViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class MainViewModel(
|
||||
private val database: AppDatabase,
|
||||
private val notificationManager: NockNotificationManager,
|
||||
private val validationManager: ValidationExecutor,
|
||||
mainDispatcher: CoroutineDispatcher,
|
||||
private val ioDispatcher: CoroutineDispatcher
|
||||
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
|
||||
|
||||
private val sites = MutableLiveData<List<Site>>()
|
||||
private val isLoading = MutableLiveData<Boolean>()
|
||||
private val emptyTextVisibility = MutableLiveData<Boolean>()
|
||||
private val tags = MutableLiveData<List<String>>()
|
||||
private val tagsListVisibility = MutableLiveData<Boolean>()
|
||||
|
||||
@CheckResult fun onSites(): LiveData<List<Site>> = sites
|
||||
|
||||
@CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading
|
||||
|
||||
@CheckResult fun onEmptyTextVisibility(): LiveData<Boolean> = emptyTextVisibility
|
||||
|
||||
@CheckResult fun onTags(): LiveData<List<String>> = tags
|
||||
|
||||
@CheckResult fun onTagsListVisibility(): LiveData<Boolean> = tagsListVisibility
|
||||
|
||||
@OnLifecycleEvent(ON_RESUME)
|
||||
fun onResume() = loadSites(emptyList())
|
||||
|
||||
fun onTagSelection(tags: List<String>) = loadSites(tags)
|
||||
|
||||
fun postSiteUpdate(model: Site) {
|
||||
val currentSites = sites.value ?: return
|
||||
val index = currentSites.indexOfFirst { it.id == model.id }
|
||||
if (index == -1) return
|
||||
sites.value = currentSites.toMutableList()
|
||||
.apply {
|
||||
this[index] = model
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshSite(model: Site) {
|
||||
validationManager.scheduleValidation(
|
||||
site = model,
|
||||
rightNow = true,
|
||||
cancelPrevious = true
|
||||
)
|
||||
}
|
||||
|
||||
fun removeSite(model: Site) {
|
||||
validationManager.cancelScheduledValidation(model)
|
||||
notificationManager.cancelStatusNotification(model)
|
||||
|
||||
scope.launch {
|
||||
isLoading.value = true
|
||||
withContext(ioDispatcher) { database.deleteSite(model) }
|
||||
|
||||
val currentSites = sites.value ?: return@launch
|
||||
val index = currentSites.indexOfFirst { it.id == model.id }
|
||||
isLoading.value = false
|
||||
if (index == -1) return@launch
|
||||
|
||||
val newSitesList = currentSites.toMutableList()
|
||||
.apply {
|
||||
removeAt(index)
|
||||
}
|
||||
sites.value = newSitesList
|
||||
emptyTextVisibility.value = newSitesList.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSites(forTags: List<String>) {
|
||||
scope.launch {
|
||||
notificationManager.cancelStatusNotifications()
|
||||
emptyTextVisibility.value = false
|
||||
isLoading.value = true
|
||||
|
||||
val unfiltered = withContext(ioDispatcher) {
|
||||
database.allSites()
|
||||
}
|
||||
var result = unfiltered
|
||||
|
||||
if (forTags.isNotEmpty()) {
|
||||
result = result.filter { site ->
|
||||
val itemTags = site.tags.toLowerCase()
|
||||
.split(",")
|
||||
itemTags.any { tag -> forTags.contains(tag) }
|
||||
}
|
||||
}
|
||||
|
||||
sites.value = result
|
||||
ensureCheckJobs()
|
||||
isLoading.value = false
|
||||
emptyTextVisibility.value = result.isEmpty()
|
||||
|
||||
val tagsValues = pullOutTags(unfiltered)
|
||||
tags.value = tagsValues
|
||||
tagsListVisibility.value = tagsValues.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ensureCheckJobs() {
|
||||
withContext(ioDispatcher) {
|
||||
validationManager.ensureScheduledValidations()
|
||||
}
|
||||
}
|
||||
|
||||
private fun pullOutTags(sites: List<Site>): List<String> {
|
||||
return mutableListOf<String>().apply {
|
||||
for (site in sites) {
|
||||
val splitTags = site.tags.toLowerCase()
|
||||
.split(',')
|
||||
splitTags
|
||||
.filter { it.isNotEmpty() }
|
||||
.forEach { tag ->
|
||||
if (!this.contains(tag)) {
|
||||
this.add(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,104 +1,184 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.viewsite
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.Intent.ACTION_OPEN_DOCUMENT
|
||||
import android.content.Intent.CATEGORY_OPENABLE
|
||||
import android.os.Bundle
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.LAST_CHECK_NONE
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ValidationMode
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.indexToValidationMode
|
||||
import com.afollestad.nocknock.data.textRes
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.formatDate
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
import com.afollestad.nocknock.utilities.ext.safeRegisterReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.safeUnregisterReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
|
||||
import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.ValidationMode
|
||||
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
||||
import com.afollestad.nocknock.utilities.ext.onTextChanged
|
||||
import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
|
||||
import com.afollestad.nocknock.utilities.livedata.distinct
|
||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
|
||||
import com.afollestad.nocknock.viewcomponents.ext.onItemSelected
|
||||
import com.afollestad.nocknock.viewcomponents.ext.isVisibleCondition
|
||||
import com.afollestad.nocknock.viewcomponents.ext.onScroll
|
||||
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
|
||||
import com.afollestad.nocknock.viewcomponents.ext.trimmedText
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||
import com.afollestad.vvalidator.form
|
||||
import com.afollestad.vvalidator.form.Form
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalLayout
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.disableChecksButton
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.doneBtn
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.headersLayout
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.iconStatus
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.inputName
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.inputTags
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.inputUrl
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.loadingProgress
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.responseTimeoutInput
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.responseValidationMode
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.responseValidationSearchTerm
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.rootView
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.retryPolicyLayout
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.scriptInputLayout
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.scrollView
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.sslCertificateBrowse
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.sslCertificateInput
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.textLastCheckResult
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.toolbar
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import kotlinx.android.synthetic.main.include_app_bar.app_toolbar as appToolbar
|
||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
|
||||
class ViewSiteActivity : DarkModeSwitchActivity() {
|
||||
companion object {
|
||||
private const val SELECT_CERT_FILE_RQ = 23
|
||||
}
|
||||
|
||||
@Inject lateinit var presenter: ViewSitePresenter
|
||||
internal val viewModel by viewModel<ViewSiteViewModel>()
|
||||
private lateinit var validationForm: Form
|
||||
|
||||
private val intentReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
intent: Intent
|
||||
) = presenter.onBroadcast(intent)
|
||||
private val intentProvider by inject<IntentProvider>()
|
||||
private val statusUpdateReceiver by lazy {
|
||||
StatusUpdateIntentReceiver(application, intentProvider) {
|
||||
viewModel.setModel(it)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
injector().injectInto(this)
|
||||
setContentView(R.layout.activity_viewsite)
|
||||
|
||||
// Populate view model with initial data
|
||||
val model = intent.getSerializableExtra(KEY_SITE) as Site
|
||||
viewModel.setModel(model)
|
||||
|
||||
setupUi()
|
||||
setupValidation()
|
||||
lifecycle.run {
|
||||
addObserver(viewModel)
|
||||
addObserver(statusUpdateReceiver)
|
||||
}
|
||||
|
||||
// Loading
|
||||
loadingProgress.observe(this, viewModel.onIsLoading())
|
||||
|
||||
// Status
|
||||
viewModel.status.observe(this, Observer {
|
||||
iconStatus.setStatus(it)
|
||||
invalidateMenuForStatus(it)
|
||||
})
|
||||
|
||||
// Name
|
||||
inputName.attachLiveData(this, viewModel.name)
|
||||
|
||||
// Tags
|
||||
inputTags.attachLiveData(this, viewModel.tags)
|
||||
|
||||
// Url
|
||||
inputUrl.attachLiveData(this, viewModel.url)
|
||||
viewModel.onUrlWarningVisibility()
|
||||
.toViewVisibility(this, textUrlWarning)
|
||||
|
||||
// Timeout
|
||||
responseTimeoutInput.attachLiveData(this, viewModel.timeout)
|
||||
|
||||
// Validation mode
|
||||
responseValidationMode.attachLiveData(
|
||||
lifecycleOwner = this,
|
||||
data = viewModel.validationMode,
|
||||
outTransformer = { ValidationMode.fromIndex(it) },
|
||||
inTransformer = { it.toIndex() }
|
||||
)
|
||||
viewModel.onValidationModeDescription()
|
||||
.toViewText(this, validationModeDescription)
|
||||
|
||||
// Validation search term
|
||||
responseValidationSearchTerm.attachLiveData(this, viewModel.validationSearchTerm)
|
||||
viewModel.onValidationSearchTermVisibility()
|
||||
.toViewVisibility(this, responseValidationSearchTerm)
|
||||
|
||||
// SSL certificate
|
||||
sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it }
|
||||
viewModel.certificateUri.distinct()
|
||||
.observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) })
|
||||
|
||||
// Headers
|
||||
headersLayout.attach(viewModel.headers)
|
||||
|
||||
// Last/next check
|
||||
viewModel.onLastCheckResultText()
|
||||
.toViewText(this, textLastCheckResult)
|
||||
viewModel.onNextCheckText()
|
||||
.toViewText(this, textNextCheck)
|
||||
}
|
||||
|
||||
private fun setupUi() {
|
||||
toolbarTitle.text = ""
|
||||
toolbar.run {
|
||||
setNavigationIcon(R.drawable.ic_action_close)
|
||||
setNavigationOnClickListener { finish() }
|
||||
inflateMenu(R.menu.menu_viewsite)
|
||||
|
||||
menu.findItem(R.id.refresh)
|
||||
.setActionView(R.layout.menu_item_refresh_icon)
|
||||
.apply {
|
||||
actionView.setOnClickListener { presenter.checkNow() }
|
||||
actionView.setOnClickListener { viewModel.checkNow() }
|
||||
}
|
||||
|
||||
setOnMenuItemClickListener {
|
||||
maybeRemoveSite()
|
||||
return@setOnMenuItemClickListener true
|
||||
when (it.itemId) {
|
||||
R.id.remove -> maybeRemoveSite()
|
||||
R.id.disableChecks -> maybeDisableChecks()
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
scrollView.onScroll {
|
||||
toolbar.elevation = if (it > toolbar.height / 4) {
|
||||
toolbar.dimenFloat(R.dimen.default_elevation)
|
||||
appToolbar.elevation = if (it > appToolbar.measuredHeight / 2) {
|
||||
appToolbar.dimenFloat(R.dimen.default_elevation)
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
|
||||
inputUrl.setOnFocusChangeListener { _, hasFocus ->
|
||||
presenter.onUrlInputFocusChange(hasFocus, inputUrl.trimmedText())
|
||||
}
|
||||
|
||||
val validationOptionsAdapter = ArrayAdapter(
|
||||
this,
|
||||
R.layout.list_item_spinner,
|
||||
|
@ -107,150 +187,105 @@ class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
|
|||
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
||||
responseValidationMode.adapter = validationOptionsAdapter
|
||||
|
||||
responseValidationMode.onItemSelected(presenter::onValidationModeSelected)
|
||||
// Disabled button
|
||||
viewModel.onDisableChecksVisibility()
|
||||
.observe(this, Observer {
|
||||
toolbar.menu.findItem(R.id.disableChecks)
|
||||
.isVisible = it
|
||||
})
|
||||
|
||||
doneBtn.setOnClickListener {
|
||||
val checkInterval = checkIntervalLayout.getSelectedCheckInterval()
|
||||
val validationMode =
|
||||
responseValidationMode.selectedItemPosition.indexToValidationMode()
|
||||
// Done item text
|
||||
viewModel.onDoneButtonText()
|
||||
.observe(this, Observer {
|
||||
toolbar.menu.findItem(R.id.commit)
|
||||
.setTitle(it)
|
||||
})
|
||||
|
||||
presenter.commit(
|
||||
name = inputName.trimmedText(),
|
||||
url = inputUrl.trimmedText(),
|
||||
checkInterval = checkInterval,
|
||||
validationMode = validationMode,
|
||||
validationContent = validationMode.validationContent()
|
||||
)
|
||||
// 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() }
|
||||
}
|
||||
}
|
||||
|
||||
disableChecksButton.setOnClickListener { maybeDisableChecks() }
|
||||
// Validation script
|
||||
scriptInputLayout.attach(
|
||||
codeData = viewModel.validationScript,
|
||||
visibility = viewModel.onValidationScriptVisibility(),
|
||||
form = validationForm
|
||||
)
|
||||
|
||||
presenter.takeView(this, intent)
|
||||
// Check interval
|
||||
checkIntervalLayout.attach(
|
||||
valueData = viewModel.checkIntervalValue,
|
||||
multiplierData = viewModel.checkIntervalUnit,
|
||||
form = validationForm
|
||||
)
|
||||
|
||||
// Retry Policy
|
||||
retryPolicyLayout.attach(
|
||||
timesData = viewModel.retryPolicyTimes,
|
||||
minutesData = viewModel.retryPolicyMinutes,
|
||||
form = validationForm
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {
|
||||
appToolbar.dimenFloat(R.dimen.default_elevation)
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
resultData: Intent?
|
||||
) {
|
||||
super.onActivityResult(requestCode, resultCode, resultData)
|
||||
if (requestCode == SELECT_CERT_FILE_RQ && resultCode == RESULT_OK) {
|
||||
sslCertificateInput.setText(resultData?.data?.toString() ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
presenter.onNewIntent(intent)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
presenter.dropView()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun setLoading() = loadingProgress.setLoading()
|
||||
|
||||
override fun setDoneLoading() = loadingProgress.setDone()
|
||||
|
||||
override fun showOrHideUrlSchemeWarning(show: Boolean) {
|
||||
textUrlWarning.showOrHide(show)
|
||||
if (show) {
|
||||
textUrlWarning.setText(R.string.warning_http_url)
|
||||
if (intent != null && intent.hasExtra(KEY_SITE)) {
|
||||
val newModel = intent.getSerializableExtra(KEY_SITE) as Site
|
||||
viewModel.setModel(newModel)
|
||||
}
|
||||
}
|
||||
|
||||
override fun showOrHideValidationSearchTerm(show: Boolean) =
|
||||
responseValidationSearchTerm.showOrHide(show)
|
||||
|
||||
override fun showOrHideScriptInput(show: Boolean) = scriptInputLayout.showOrHide(show)
|
||||
|
||||
override fun setValidationModeDescription(res: Int) = validationModeDescription.setText(res)
|
||||
|
||||
override fun displayModel(model: ServerModel) = with(model) {
|
||||
iconStatus.setStatus(this.status)
|
||||
inputName.setText(this.name)
|
||||
inputUrl.setText(this.url)
|
||||
|
||||
if (this.lastCheck == LAST_CHECK_NONE) {
|
||||
textLastCheckResult.setText(R.string.none)
|
||||
} else {
|
||||
val statusText = this.status.textRes()
|
||||
textLastCheckResult.text = if (statusText == 0) {
|
||||
this.reason
|
||||
} else {
|
||||
getString(statusText)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.disabled) {
|
||||
textNextCheck.setText(R.string.auto_checks_disabled)
|
||||
} else {
|
||||
textNextCheck.text = (this.lastCheck + this.checkInterval).formatDate()
|
||||
}
|
||||
checkIntervalLayout.set(this.checkInterval)
|
||||
|
||||
responseValidationMode.setSelection(validationMode.value - 1)
|
||||
when (this.validationMode) {
|
||||
TERM_SEARCH -> responseValidationSearchTerm.setText(this.validationContent ?: "")
|
||||
JAVASCRIPT -> scriptInputLayout.setCode(this.validationContent)
|
||||
else -> {
|
||||
responseValidationSearchTerm.setText("")
|
||||
scriptInputLayout.clear()
|
||||
}
|
||||
}
|
||||
|
||||
disableChecksButton.showOrHide(!this.disabled)
|
||||
doneBtn.setText(
|
||||
if (this.disabled) R.string.renable_and_save_changes
|
||||
else R.string.save_changes
|
||||
)
|
||||
|
||||
invalidateMenuForStatus(model)
|
||||
}
|
||||
|
||||
override fun setInputErrors(errors: InputErrors) {
|
||||
inputName.error = if (errors.name != null) {
|
||||
getString(errors.name!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
inputUrl.error = if (errors.url != null) {
|
||||
getString(errors.url!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
checkIntervalLayout.setError(
|
||||
if (errors.checkInterval != null) {
|
||||
getString(errors.checkInterval!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
responseValidationSearchTerm.error = if (errors.termSearch != null) {
|
||||
getString(errors.termSearch!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
scriptInputLayout.setError(
|
||||
if (errors.javaScript != null) {
|
||||
getString(errors.javaScript!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun scopeWhileAttached(
|
||||
context: CoroutineContext,
|
||||
exec: ScopeReceiver
|
||||
) = rootView.scopeWhileAttached(context, exec)
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val filter = IntentFilter().apply {
|
||||
addAction(ACTION_STATUS_UPDATE)
|
||||
}
|
||||
safeRegisterReceiver(intentReceiver, filter)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
safeUnregisterReceiver(intentReceiver)
|
||||
}
|
||||
|
||||
private fun ValidationMode.validationContent() = when (this) {
|
||||
STATUS_CODE -> null
|
||||
TERM_SEARCH -> responseValidationSearchTerm.trimmedText()
|
||||
JAVASCRIPT -> scriptInputLayout.getCode()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,46 +1,59 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.viewsite
|
||||
|
||||
import android.widget.ImageView
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.isPending
|
||||
import com.afollestad.nocknock.data.model.Status
|
||||
import com.afollestad.nocknock.data.model.isPending
|
||||
import com.afollestad.nocknock.toHtml
|
||||
import com.afollestad.nocknock.utilities.ext.animateRotation
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.toolbar
|
||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||
|
||||
const val KEY_SITE = "site_model"
|
||||
|
||||
internal fun ViewSiteActivity.maybeRemoveSite() {
|
||||
val model = presenter.currentModel()
|
||||
val model = viewModel.site
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.remove_site)
|
||||
message(text = context.getString(R.string.remove_site_prompt, model.name).toHtml())
|
||||
positiveButton(R.string.remove) { presenter.removeSite() }
|
||||
positiveButton(R.string.remove) {
|
||||
viewModel.removeSite { finish() }
|
||||
}
|
||||
negativeButton(android.R.string.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ViewSiteActivity.maybeDisableChecks() {
|
||||
val model = presenter.currentModel()
|
||||
val model = viewModel.site
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.disable_automatic_checks)
|
||||
message(
|
||||
text = context.getString(R.string.disable_automatic_checks_prompt, model.name).toHtml()
|
||||
)
|
||||
positiveButton(R.string.disable) { presenter.disableChecks() }
|
||||
positiveButton(R.string.disable) { viewModel.disableSite() }
|
||||
negativeButton(android.R.string.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ViewSiteActivity.invalidateMenuForStatus(model: ServerModel) {
|
||||
internal fun ViewSiteActivity.invalidateMenuForStatus(status: Status) {
|
||||
val refreshIcon = toolbar.menu.findItem(R.id.refresh)
|
||||
.actionView as ImageView
|
||||
|
||||
if (model.status.isPending()) {
|
||||
if (status.isPending()) {
|
||||
refreshIcon.animateRotation()
|
||||
} else {
|
||||
refreshIcon.run {
|
||||
|
|
|
@ -1,264 +0,0 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.viewsite
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.annotation.CheckResult
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
||||
import com.afollestad.nocknock.data.ValidationMode
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.HttpUrl
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
import javax.inject.Inject
|
||||
|
||||
const val KEY_VIEW_MODEL = "site_model"
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
data class InputErrors(
|
||||
var name: Int? = null,
|
||||
var url: Int? = null,
|
||||
var checkInterval: Int? = null,
|
||||
var termSearch: Int? = null,
|
||||
var javaScript: Int? = null
|
||||
) {
|
||||
@CheckResult fun any(): Boolean {
|
||||
return name != null || url != null || checkInterval != null ||
|
||||
termSearch != null || javaScript != null
|
||||
}
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
interface ViewSitePresenter {
|
||||
|
||||
fun takeView(
|
||||
view: ViewSiteView,
|
||||
intent: Intent
|
||||
)
|
||||
|
||||
fun onBroadcast(intent: Intent)
|
||||
|
||||
fun onNewIntent(intent: Intent?)
|
||||
|
||||
fun onUrlInputFocusChange(
|
||||
focused: Boolean,
|
||||
content: String
|
||||
)
|
||||
|
||||
fun onValidationModeSelected(index: Int)
|
||||
|
||||
fun commit(
|
||||
name: String,
|
||||
url: String,
|
||||
checkInterval: Long,
|
||||
validationMode: ValidationMode,
|
||||
validationContent: String?
|
||||
)
|
||||
|
||||
fun checkNow()
|
||||
|
||||
fun disableChecks()
|
||||
|
||||
fun removeSite()
|
||||
|
||||
fun currentModel(): ServerModel
|
||||
|
||||
fun dropView()
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class RealViewSitePresenter @Inject constructor(
|
||||
private val serverModelStore: ServerModelStore,
|
||||
private val checkStatusManager: CheckStatusManager,
|
||||
private val notificationManager: NockNotificationManager
|
||||
) : ViewSitePresenter {
|
||||
|
||||
private var view: ViewSiteView? = null
|
||||
private var currentModel: ServerModel? = null
|
||||
|
||||
override fun takeView(
|
||||
view: ViewSiteView,
|
||||
intent: Intent
|
||||
) {
|
||||
this.currentModel = intent.getSerializableExtra(KEY_VIEW_MODEL) as ServerModel
|
||||
this.view = view.apply {
|
||||
displayModel(currentModel!!)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBroadcast(intent: Intent) {
|
||||
if (intent.action == ACTION_STATUS_UPDATE) {
|
||||
val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? ServerModel ?: return
|
||||
this.currentModel = model
|
||||
view?.displayModel(model)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
if (intent != null && intent.hasExtra(KEY_VIEW_MODEL)) {
|
||||
currentModel = intent.getSerializableExtra(KEY_VIEW_MODEL) as ServerModel
|
||||
view?.displayModel(currentModel!!)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUrlInputFocusChange(
|
||||
focused: Boolean,
|
||||
content: String
|
||||
) {
|
||||
if (content.isEmpty() || focused) {
|
||||
return
|
||||
}
|
||||
val url = HttpUrl.parse(content)
|
||||
if (url == null ||
|
||||
(url.scheme() != "http" &&
|
||||
url.scheme() != "https")
|
||||
) {
|
||||
view?.showOrHideUrlSchemeWarning(true)
|
||||
} else {
|
||||
view?.showOrHideUrlSchemeWarning(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onValidationModeSelected(index: Int) = with(view!!) {
|
||||
showOrHideValidationSearchTerm(index == 1)
|
||||
showOrHideScriptInput(index == 2)
|
||||
setValidationModeDescription(
|
||||
when (index) {
|
||||
0 -> R.string.validation_mode_status_desc
|
||||
1 -> R.string.validation_mode_term_desc
|
||||
2 -> R.string.validation_mode_javascript_desc
|
||||
else -> throw IllegalStateException("Unknown validation mode position: $index")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun commit(
|
||||
name: String,
|
||||
url: String,
|
||||
checkInterval: Long,
|
||||
validationMode: ValidationMode,
|
||||
validationContent: String?
|
||||
) {
|
||||
val inputErrors = InputErrors()
|
||||
|
||||
if (name.isEmpty()) {
|
||||
inputErrors.name = R.string.please_enter_name
|
||||
}
|
||||
if (url.isEmpty()) {
|
||||
inputErrors.url = R.string.please_enter_url
|
||||
} else if (HttpUrl.parse(url) == null) {
|
||||
inputErrors.url = R.string.please_enter_valid_url
|
||||
}
|
||||
if (checkInterval <= 0) {
|
||||
inputErrors.checkInterval = R.string.please_enter_check_interval
|
||||
}
|
||||
if (validationMode == TERM_SEARCH && validationContent.isNullOrEmpty()) {
|
||||
inputErrors.termSearch = R.string.please_enter_search_term
|
||||
} else if (validationMode == JAVASCRIPT && validationContent.isNullOrEmpty()) {
|
||||
inputErrors.javaScript = R.string.please_enter_javaScript
|
||||
}
|
||||
|
||||
if (inputErrors.any()) {
|
||||
view?.setInputErrors(inputErrors)
|
||||
return
|
||||
}
|
||||
|
||||
val newModel = currentModel!!.copy(
|
||||
name = name,
|
||||
url = url,
|
||||
status = WAITING,
|
||||
checkInterval = checkInterval,
|
||||
validationMode = validationMode,
|
||||
validationContent = validationContent,
|
||||
disabled = false
|
||||
)
|
||||
|
||||
with(view!!) {
|
||||
scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
setLoading()
|
||||
async(IO) { serverModelStore.update(newModel) }.await()
|
||||
checkStatusManager.scheduleCheck(
|
||||
site = newModel,
|
||||
rightNow = true,
|
||||
cancelPrevious = true
|
||||
)
|
||||
setDoneLoading()
|
||||
view?.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkNow() = with(view!!) {
|
||||
val checkModel = currentModel!!.copy(
|
||||
status = WAITING
|
||||
)
|
||||
view?.displayModel(checkModel)
|
||||
checkStatusManager.scheduleCheck(
|
||||
site = checkModel,
|
||||
rightNow = true,
|
||||
cancelPrevious = true
|
||||
)
|
||||
}
|
||||
|
||||
override fun disableChecks() {
|
||||
val site = currentModel!!
|
||||
checkStatusManager.cancelCheck(site)
|
||||
notificationManager.cancelStatusNotification(site)
|
||||
|
||||
with(view!!) {
|
||||
scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
setLoading()
|
||||
currentModel = currentModel!!.copy(disabled = true)
|
||||
async(IO) { serverModelStore.update(currentModel!!) }.await()
|
||||
setDoneLoading()
|
||||
view?.displayModel(currentModel!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeSite() {
|
||||
val site = currentModel!!
|
||||
checkStatusManager.cancelCheck(site)
|
||||
notificationManager.cancelStatusNotification(site)
|
||||
|
||||
with(view!!) {
|
||||
scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
setLoading()
|
||||
async(IO) { serverModelStore.delete(site) }.await()
|
||||
setDoneLoading()
|
||||
view?.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun currentModel() = this.currentModel!!
|
||||
|
||||
override fun dropView() {
|
||||
view = null
|
||||
currentModel = null
|
||||
}
|
||||
|
||||
@TestOnly fun setModel(model: ServerModel) {
|
||||
this.currentModel = model
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.viewsite
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
interface ViewSiteView {
|
||||
|
||||
fun setLoading()
|
||||
|
||||
fun setDoneLoading()
|
||||
|
||||
fun displayModel(model: ServerModel)
|
||||
|
||||
fun showOrHideUrlSchemeWarning(show: Boolean)
|
||||
|
||||
fun showOrHideValidationSearchTerm(show: Boolean)
|
||||
|
||||
fun showOrHideScriptInput(show: Boolean)
|
||||
|
||||
fun setValidationModeDescription(@StringRes res: Int)
|
||||
|
||||
fun setInputErrors(errors: InputErrors)
|
||||
|
||||
fun scopeWhileAttached(
|
||||
context: CoroutineContext,
|
||||
exec: ScopeReceiver
|
||||
)
|
||||
|
||||
fun finish()
|
||||
}
|
|
@ -0,0 +1,268 @@
|
|||
/**
|
||||
* 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.viewsite
|
||||
|
||||
import androidx.annotation.CheckResult
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.VisibleForTesting.PRIVATE
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.AppDatabase
|
||||
import com.afollestad.nocknock.data.deleteSite
|
||||
import com.afollestad.nocknock.data.model.Header
|
||||
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.Status
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
import com.afollestad.nocknock.data.model.ValidationMode
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.model.ValidationResult
|
||||
import com.afollestad.nocknock.data.model.textRes
|
||||
import com.afollestad.nocknock.data.updateSite
|
||||
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.ui.ScopedViewModel
|
||||
import com.afollestad.nocknock.utilities.ext.formatDate
|
||||
import com.afollestad.nocknock.utilities.livedata.map
|
||||
import com.afollestad.nocknock.utilities.livedata.zip
|
||||
import com.afollestad.nocknock.utilities.providers.StringProvider
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.HttpUrl
|
||||
import java.lang.System.currentTimeMillis
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class ViewSiteViewModel(
|
||||
private val stringProvider: StringProvider,
|
||||
private val database: AppDatabase,
|
||||
private val notificationManager: NockNotificationManager,
|
||||
private val validationManager: ValidationExecutor,
|
||||
mainDispatcher: CoroutineDispatcher,
|
||||
private val ioDispatcher: CoroutineDispatcher
|
||||
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
|
||||
|
||||
lateinit var site: Site
|
||||
|
||||
// Public properties
|
||||
val status = MutableLiveData<Status>()
|
||||
val name = MutableLiveData<String>()
|
||||
val tags = MutableLiveData<String>()
|
||||
val url = MutableLiveData<String>()
|
||||
val timeout = MutableLiveData<Int>()
|
||||
val validationMode = MutableLiveData<ValidationMode>()
|
||||
val validationSearchTerm = MutableLiveData<String>()
|
||||
val validationScript = MutableLiveData<String>()
|
||||
val checkIntervalValue = MutableLiveData<Int>()
|
||||
val checkIntervalUnit = MutableLiveData<Long>()
|
||||
val retryPolicyTimes = MutableLiveData<Int>()
|
||||
val retryPolicyMinutes = MutableLiveData<Int>()
|
||||
val headers = MutableLiveData<List<Header>>()
|
||||
val certificateUri = MutableLiveData<String>()
|
||||
internal val disabled = MutableLiveData<Boolean>()
|
||||
internal val lastResult = MutableLiveData<ValidationResult?>()
|
||||
|
||||
private val isLoading = MutableLiveData<Boolean>()
|
||||
|
||||
@CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading
|
||||
|
||||
@CheckResult fun onUrlWarningVisibility(): LiveData<Boolean> {
|
||||
return url.map {
|
||||
val parsed = HttpUrl.parse(it)
|
||||
return@map it.isNotEmpty() && parsed == null
|
||||
}
|
||||
}
|
||||
|
||||
@CheckResult fun onValidationModeDescription(): LiveData<Int> {
|
||||
return validationMode.map {
|
||||
when (it!!) {
|
||||
STATUS_CODE -> R.string.validation_mode_status_desc
|
||||
TERM_SEARCH -> R.string.validation_mode_term_desc
|
||||
JAVASCRIPT -> R.string.validation_mode_javascript_desc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CheckResult fun onValidationSearchTermVisibility() = validationMode.map { it == TERM_SEARCH }
|
||||
|
||||
@CheckResult fun onValidationScriptVisibility() = validationMode.map { it == JAVASCRIPT }
|
||||
|
||||
@CheckResult fun onDisableChecksVisibility(): LiveData<Boolean> = disabled.map { !it }
|
||||
|
||||
@CheckResult fun onDoneButtonText(): LiveData<Int> =
|
||||
disabled.map {
|
||||
if (it) R.string.renable_and_save_changes
|
||||
else R.string.save_changes
|
||||
}
|
||||
|
||||
@CheckResult fun onLastCheckResultText(): LiveData<String> = lastResult.map {
|
||||
if (it == null) {
|
||||
stringProvider.get(R.string.none)
|
||||
} else {
|
||||
val statusText = it.status.textRes()
|
||||
if (statusText == 0) {
|
||||
it.reason
|
||||
} else {
|
||||
stringProvider.get(statusText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CheckResult fun onNextCheckText(): LiveData<String> {
|
||||
return zip(disabled, lastResult)
|
||||
.map {
|
||||
val disabled = it.first
|
||||
val lastResult = it.second
|
||||
if (disabled) {
|
||||
stringProvider.get(R.string.auto_checks_disabled)
|
||||
} else {
|
||||
val lastCheck = lastResult?.timestampMs ?: currentTimeMillis()
|
||||
(lastCheck + getCheckIntervalMs()).formatDate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
fun commit(done: () -> Unit) {
|
||||
scope.launch {
|
||||
val updatedModel = getUpdatedDbModel() ?: return@launch
|
||||
isLoading.value = true
|
||||
|
||||
withContext(ioDispatcher) {
|
||||
database.updateSite(updatedModel)
|
||||
}
|
||||
validationManager.scheduleValidation(
|
||||
site = updatedModel,
|
||||
rightNow = true,
|
||||
cancelPrevious = true
|
||||
)
|
||||
|
||||
isLoading.value = false
|
||||
done()
|
||||
}
|
||||
}
|
||||
|
||||
fun checkNow() {
|
||||
val checkModel = site.withStatus(
|
||||
status = WAITING
|
||||
)
|
||||
setModel(checkModel)
|
||||
validationManager.scheduleValidation(
|
||||
site = checkModel,
|
||||
rightNow = true,
|
||||
cancelPrevious = true
|
||||
)
|
||||
}
|
||||
|
||||
fun removeSite(done: () -> Unit) {
|
||||
validationManager.cancelScheduledValidation(site)
|
||||
notificationManager.cancelStatusNotification(site)
|
||||
|
||||
scope.launch {
|
||||
isLoading.value = true
|
||||
withContext(ioDispatcher) {
|
||||
database.deleteSite(site)
|
||||
}
|
||||
isLoading.value = false
|
||||
done()
|
||||
}
|
||||
}
|
||||
|
||||
fun disableSite() {
|
||||
validationManager.cancelScheduledValidation(site)
|
||||
notificationManager.cancelStatusNotification(site)
|
||||
|
||||
scope.launch {
|
||||
isLoading.value = true
|
||||
val newModel = site.copy(
|
||||
settings = site.settings!!.copy(
|
||||
disabled = true
|
||||
)
|
||||
)
|
||||
withContext(ioDispatcher) {
|
||||
database.updateSite(newModel)
|
||||
}
|
||||
isLoading.value = false
|
||||
setModel(newModel)
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
@VisibleForTesting(otherwise = PRIVATE)
|
||||
fun getCheckIntervalMs(): Long {
|
||||
val value = checkIntervalValue.value ?: return 0
|
||||
val unit = checkIntervalUnit.value ?: return 0
|
||||
return value * unit
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = PRIVATE)
|
||||
fun getValidationArgs(): String? {
|
||||
return when (validationMode.value) {
|
||||
TERM_SEARCH -> validationSearchTerm.value?.trim()
|
||||
JAVASCRIPT -> validationScript.value?.trim()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUpdatedDbModel(): Site? {
|
||||
val timeout = timeout.value ?: 10_000
|
||||
val cleanedTags = tags.value?.split(',')?.joinToString(separator = ",") ?: ""
|
||||
|
||||
val newSettings = site.settings!!.copy(
|
||||
validationIntervalMs = getCheckIntervalMs(),
|
||||
validationMode = validationMode.value!!,
|
||||
validationArgs = getValidationArgs(),
|
||||
networkTimeout = timeout,
|
||||
disabled = false,
|
||||
certificate = certificateUri.value?.toString()
|
||||
)
|
||||
|
||||
val retryPolicyTimes = retryPolicyTimes.value ?: 0
|
||||
val retryPolicyMinutes = retryPolicyMinutes.value ?: 0
|
||||
val retryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) {
|
||||
if (site.retryPolicy != null) {
|
||||
// Have existing policy, update it
|
||||
site.retryPolicy!!.copy(
|
||||
count = retryPolicyTimes,
|
||||
minutes = retryPolicyMinutes
|
||||
)
|
||||
} else {
|
||||
// Create new policy
|
||||
RetryPolicy(
|
||||
count = retryPolicyTimes,
|
||||
minutes = retryPolicyMinutes
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// No policy
|
||||
null
|
||||
}
|
||||
|
||||
return site.copy(
|
||||
name = name.value!!.trim(),
|
||||
tags = cleanedTags,
|
||||
url = url.value!!.trim(),
|
||||
settings = newSettings,
|
||||
retryPolicy = retryPolicy,
|
||||
headers = headers.value ?: emptyList()
|
||||
)
|
||||
.withStatus(status = WAITING)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* 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.viewsite
|
||||
|
||||
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
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 ViewSiteViewModel.setModel(site: Site) {
|
||||
val settings = site.settings ?: throw IllegalArgumentException("Settings must be populated!")
|
||||
this.site = site
|
||||
|
||||
status.value = site.lastResult?.status ?: WAITING
|
||||
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
|
||||
if (settings.certificate == "null") {
|
||||
certificateUri.value = ""
|
||||
} else {
|
||||
certificateUri.value = settings.certificate
|
||||
}
|
||||
|
||||
this.disabled.value = settings.disabled
|
||||
this.lastResult.value = site.lastResult
|
||||
}
|
||||
|
||||
private fun ViewSiteViewModel.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 ViewSiteViewModel.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()
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:fillAfter="true">
|
||||
<alpha
|
||||
android:duration="400"
|
||||
android:fromAlpha="1.0"
|
||||
android:interpolator="@android:anim/accelerate_interpolator"
|
||||
android:toAlpha="0.0"/>
|
||||
</set>
|
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>
|
|
@ -1,4 +1,4 @@
|
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<size android:height="1dp"/>
|
||||
<solid android:color="@color/dividerColor"/>
|
||||
<solid android:color="?dividerColor"/>
|
||||
</shape>
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:fillColor="?iconColor"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:fillColor="?iconColor"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:fillColor="?iconColor"
|
||||
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
|
||||
</vector>
|
||||
|
|
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>
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
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:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -11,21 +10,13 @@
|
|||
android:id="@+id/rootView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?colorPrimary"
|
||||
android:orientation="vertical"
|
||||
>
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/FlatToolbarTheme"
|
||||
app:navigationIcon="@drawable/ic_action_close"
|
||||
app:title="@string/add_site"
|
||||
app:titleTextColor="#FFFFFF"
|
||||
/>
|
||||
<include layout="@layout/include_app_bar"/>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
>
|
||||
|
@ -34,66 +25,66 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/content_inset"
|
||||
android:paddingBottom="@dimen/content_inset_double"
|
||||
android:paddingLeft="@dimen/content_inset"
|
||||
android:paddingRight="@dimen/content_inset"
|
||||
android:paddingTop="@dimen/content_inset_half"
|
||||
>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/nameTiLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="-4dp"
|
||||
android:layout_marginRight="-4dp"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
>
|
||||
<TextView
|
||||
android:layout_marginTop="0dp"
|
||||
android:text="@string/site_name"
|
||||
style="@style/InputForm.Header"
|
||||
/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/inputName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/site_name"
|
||||
android:inputType="textPersonName|textCapWords|textAutoCorrect"
|
||||
android:textColor="#FFFFFF"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
<EditText
|
||||
android:id="@+id/inputName"
|
||||
android:hint="@string/site_name_hint"
|
||||
android:inputType="textPersonName|textCapWords|textAutoCorrect"
|
||||
android:nextFocusDown="@+id/inputUrl"
|
||||
tools:ignore="Autofill"
|
||||
style="@style/InputForm.Field"
|
||||
/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<TextView
|
||||
android:text="@string/site_url"
|
||||
style="@style/InputForm.Header"
|
||||
/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/urlTiLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="-4dp"
|
||||
android:layout_marginRight="-4dp"
|
||||
android:layout_marginTop="@dimen/content_inset_half"
|
||||
>
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/inputUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/site_url"
|
||||
android:inputType="textUri"
|
||||
android:textColor="#FFFFFF"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
<EditText
|
||||
android:id="@+id/inputUrl"
|
||||
android:hint="@string/site_url_hint"
|
||||
android:inputType="textUri"
|
||||
android:nextFocusDown="@+id/inputTags"
|
||||
tools:ignore="Autofill"
|
||||
style="@style/InputForm.Field"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textUrlWarning"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/list_text_spacing"
|
||||
android:text="@string/warning_http_url"
|
||||
android:visibility="gone"
|
||||
tools:text="Warning: this app checks for server availability with HTTP requests. It's recommended that you use an HTTP URL."
|
||||
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"/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.CheckIntervalLayout
|
||||
<com.afollestad.nocknock.viewcomponents.interval.ValidationIntervalLayout
|
||||
android:id="@+id/checkIntervalLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -102,11 +93,8 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/responseValidationLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:text="@string/response_validation_mode"
|
||||
style="@style/NockText.SectionHeader"
|
||||
style="@style/InputForm.Header"
|
||||
/>
|
||||
|
||||
<Spinner
|
||||
|
@ -126,16 +114,16 @@
|
|||
android:hint="@string/search_term"
|
||||
android:visibility="gone"
|
||||
tools:ignore="Autofill,TextFields"
|
||||
style="@style/NockText.Body.Light"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.JavaScriptInputLayout
|
||||
<com.afollestad.nocknock.viewcomponents.js.JavaScriptInputLayout
|
||||
android:id="@+id/scriptInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/content_inset"
|
||||
android:layout_marginTop="@dimen/content_inset_half"
|
||||
android:background="@color/colorPrimaryDark"
|
||||
android:background="?scriptLayoutBackground"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
|
@ -148,13 +136,75 @@
|
|||
style="@style/NockText.Body.Light"
|
||||
/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/doneBtn"
|
||||
<include layout="@layout/include_divider"/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.retrypolicy.RetryPolicyLayout
|
||||
android:id="@+id/retryPolicyLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset_double"
|
||||
android:text="@string/add_site"
|
||||
style="@style/AccentButton"
|
||||
android:layout_marginTop="@dimen/content_inset_more"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:text="@string/response_timeout"
|
||||
style="@style/InputForm.Header"
|
||||
/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/responseTimeoutInput"
|
||||
android:hint="@string/response_timeout_default"
|
||||
android:inputType="number"
|
||||
android:maxLength="8"
|
||||
tools:ignore="Autofill"
|
||||
style="@style/InputForm.Field"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:text="@string/ssl_certificate"
|
||||
style="@style/NockText.SectionHeader"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/sslCertificateInput"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:layout_marginStart="-4dp"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/ssl_certificate_automatic"
|
||||
android:inputType="textUri"
|
||||
tools:ignore="Autofill,HardcodedText,LabelFor"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/sslCertificateBrowse"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:text="@string/ssl_certificate_browse"
|
||||
style="@style/AccentTextButton"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include layout="@layout/include_divider"/>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -15,12 +15,19 @@
|
|||
android:orientation="vertical"
|
||||
>
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
<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:theme="@style/MainToolbarTheme"
|
||||
style="@style/MainToolbarStyle"
|
||||
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
|
||||
|
@ -34,18 +41,31 @@
|
|||
|
||||
<include layout="@layout/include_empty_view"/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:layout_margin="@dimen/content_inset"
|
||||
android:src="@drawable/ic_add"
|
||||
app:backgroundTint="?colorAccent"
|
||||
app:elevation="@dimen/fab_elevation"
|
||||
app:fabSize="normal"
|
||||
app:pressedTranslationZ="@dimen/fab_elevation_pressed"
|
||||
app:rippleColor="#40ffffff"
|
||||
android:layout_marginBottom="@dimen/content_inset"
|
||||
android:layout_marginEnd="@dimen/content_inset_more"
|
||||
android:minHeight="64dp"
|
||||
android:paddingBottom="@dimen/content_inset_half"
|
||||
android:paddingEnd="@dimen/content_inset"
|
||||
android:paddingStart="@dimen/content_inset"
|
||||
android:paddingTop="@dimen/content_inset_half"
|
||||
android:text="@string/add_site"
|
||||
app:cornerRadius="32dp"
|
||||
app:icon="@drawable/ic_add"
|
||||
app:iconTint="#fff"
|
||||
style="@style/Widget.MaterialComponents.Button.Icon"
|
||||
/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.LoadingIndicatorFrame
|
||||
android:id="@+id/loadingProgress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
</FrameLayout>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
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:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -15,17 +14,7 @@
|
|||
android:orientation="vertical"
|
||||
>
|
||||
|
||||
<!-- Background is applied again here so programmatic elevation works -->
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?colorPrimary"
|
||||
android:theme="@style/FlatToolbarTheme"
|
||||
app:navigationIcon="@drawable/ic_action_close"
|
||||
app:title="@string/view_site"
|
||||
app:titleTextColor="?android:textColorPrimary"
|
||||
/>
|
||||
<include layout="@layout/include_app_bar"/>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
|
@ -37,15 +26,30 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/content_inset"
|
||||
android:paddingBottom="@dimen/content_inset_double"
|
||||
android:paddingLeft="@dimen/content_inset"
|
||||
android:paddingRight="@dimen/content_inset"
|
||||
android:paddingTop="@dimen/content_inset_half"
|
||||
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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset_quarter"
|
||||
android:orientation="horizontal"
|
||||
>
|
||||
|
||||
|
@ -66,27 +70,14 @@
|
|||
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:textColor="#FFFFFF"
|
||||
android:transitionName="site_name"
|
||||
tools:ignore="Autofill,UnusedAttribute"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/inputUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/site_url"
|
||||
android:inputType="textUri"
|
||||
android:nextFocusDown="@+id/inputTags"
|
||||
android:singleLine="true"
|
||||
android:textColor="#FFFFFF"
|
||||
android:transitionName="site_url"
|
||||
tools:ignore="Autofill,UnusedAttribute"
|
||||
style="@style/NockText.Body"
|
||||
|
@ -104,6 +95,19 @@
|
|||
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>
|
||||
|
@ -115,20 +119,13 @@
|
|||
android:layout_marginTop="@dimen/content_inset_less"
|
||||
/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.CheckIntervalLayout
|
||||
<com.afollestad.nocknock.viewcomponents.interval.ValidationIntervalLayout
|
||||
android:id="@+id/checkIntervalLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
/>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="@dimen/content_inset_less"
|
||||
android:background="@color/dividerColorDark"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/responseValidationLabel"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -153,19 +150,18 @@
|
|||
android:layout_marginRight="-4dp"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:hint="@string/search_term"
|
||||
android:textColor="#FFFFFF"
|
||||
android:visibility="gone"
|
||||
tools:ignore="Autofill,TextFields"
|
||||
style="@style/NockText.Body.Light"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.JavaScriptInputLayout
|
||||
<com.afollestad.nocknock.viewcomponents.js.JavaScriptInputLayout
|
||||
android:id="@+id/scriptInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/content_inset"
|
||||
android:layout_marginTop="@dimen/content_inset_half"
|
||||
android:background="@color/colorPrimaryDark"
|
||||
android:background="?scriptLayoutBackground"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
|
@ -178,6 +174,90 @@
|
|||
style="@style/NockText.Body.Light"
|
||||
/>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="@dimen/content_inset_less"
|
||||
android:background="?dividerColor"
|
||||
/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.retrypolicy.RetryPolicyLayout
|
||||
android:id="@+id/retryPolicyLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:text="@string/response_timeout"
|
||||
style="@style/NockText.SectionHeader"
|
||||
/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/responseTimeoutInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="-4dp"
|
||||
android:layout_marginStart="-4dp"
|
||||
android:layout_marginTop="@dimen/content_inset_quarter"
|
||||
android:hint="@string/response_timeout_default"
|
||||
android:inputType="number"
|
||||
android:maxLength="8"
|
||||
tools:ignore="Autofill,HardcodedText,LabelFor"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:text="@string/ssl_certificate"
|
||||
style="@style/NockText.SectionHeader"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/sslCertificateInput"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:layout_marginStart="-4dp"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/ssl_certificate_automatic"
|
||||
android:inputType="textUri"
|
||||
tools:ignore="Autofill,HardcodedText,LabelFor"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/sslCertificateBrowse"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:text="@string/ssl_certificate_browse"
|
||||
style="@style/AccentTextButton"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include layout="@layout/include_divider"/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.headers.HeaderStackLayout
|
||||
android:id="@+id/headersLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset_half"
|
||||
/>
|
||||
|
||||
|
||||
<include layout="@layout/include_divider"/>
|
||||
|
||||
<TextView
|
||||
|
@ -214,24 +294,6 @@
|
|||
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_half"
|
||||
android:text="@string/disable_automatic_checks"
|
||||
style="@style/PrimaryDarkButton"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
|
30
app/src/main/res/layout/include_app_bar.xml
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/app_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?colorPrimary"
|
||||
android:elevation="0dp"
|
||||
android:gravity="center"
|
||||
tools:ignore="Overdraw"
|
||||
>
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?actionBarSize"
|
||||
android:elevation="0dp"
|
||||
/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/toolbar_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?actionBarSize"
|
||||
android:gravity="center"
|
||||
android:text="@string/app_name"
|
||||
android:textAppearance="@style/AppTheme.TextAppearance.Title"
|
||||
/>
|
||||
|
||||
</FrameLayout>
|
|
@ -4,5 +4,5 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:background="@color/dividerColorDark"
|
||||
android:background="?dividerColor"
|
||||
/>
|
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>
|
|
@ -3,4 +3,8 @@
|
|||
<item
|
||||
android:id="@+id/about"
|
||||
android:title="@string/about"/>
|
||||
<item
|
||||
android:id="@+id/dark_mode"
|
||||
android:checkable="true"
|
||||
android:title="@string/dark_mode"/>
|
||||
</menu>
|
||||
|
|
|
@ -1,17 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/commit"
|
||||
android:icon="@drawable/ic_check"
|
||||
android:title="@string/save_changes"
|
||||
app:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/refresh"
|
||||
android:icon="@drawable/ic_action_refresh"
|
||||
android:title="@string/refresh_status"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/remove"
|
||||
android:icon="@drawable/ic_action_delete"
|
||||
android:title="@string/remove_site"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/disableChecks"
|
||||
android:title="@string/disable_automatic_checks"
|
||||
/>
|
||||
</menu>
|
||||
|
|
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 |
9
app/src/main/res/values-v23/styles.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="AppTheme" parent="AppThemeParent">
|
||||
<item name="android:statusBarColor">@color/colorPrimary_lightTheme</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
20
app/src/main/res/values-v27/styles.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="AppTheme" parent="AppThemeParent">
|
||||
<item name="android:statusBarColor">@color/colorPrimary_lightTheme</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
|
||||
<item name="android:navigationBarColor">@color/colorPrimaryDark_lightTheme</item>
|
||||
<item name="android:windowLightNavigationBar">true</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Dark" parent="AppThemeParent.Dark">
|
||||
<item name="android:statusBarColor">@color/colorPrimary_darkTheme</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
|
||||
<item name="android:navigationBarColor">@color/colorPrimaryDark_darkTheme</item>
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
<string-array name="site_long_options" translatable="false">
|
||||
<item>@string/refresh_status</item>
|
||||
<item>@string/duplicate_and_modify</item>
|
||||
<item>@string/remove_site</item>
|
||||
</string-array>
|
||||
|
||||
|
|
9
app/src/main/res/values/attrs.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<attr format="color" name="toolbarTitleColor"/>
|
||||
<attr format="color" name="dividerColor"/>
|
||||
<attr format="color" name="iconColor"/>
|
||||
<attr format="color" name="scriptLayoutBackground"/>
|
||||
|
||||
</resources>
|
|
@ -1,10 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<color name="colorPrimary">#455A64</color>
|
||||
<color name="colorPrimaryDark">#37474F</color>
|
||||
<color name="colorAccent">#FF6E40</color>
|
||||
<color name="colorPrimary_lightTheme">#FFFFFF</color>
|
||||
<color name="colorPrimaryDark_lightTheme">#F5F5F5</color>
|
||||
|
||||
<color name="dividerColor">#EEEEEE</color>
|
||||
<color name="colorPrimary_darkTheme">#212121</color>
|
||||
<color name="colorPrimaryDark_darkTheme">#252525</color>
|
||||
|
||||
<color name="darkerGray">#303030</color>
|
||||
<color name="lighterGray">#EEEEEE</color>
|
||||
|
||||
<color name="colorAccent">#FF6E40</color>
|
||||
<color name="colorAccent_pressed">#E44615</color>
|
||||
<color name="colorAccent_translucent">#40FF6E40</color>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
<resources>
|
||||
|
||||
<dimen name="empty_text_size">28sp</dimen>
|
||||
|
||||
<dimen name="list_text_spacing">6dp</dimen>
|
||||
|
||||
<dimen name="fab_elevation">4dp</dimen>
|
||||
<dimen name="fab_elevation_pressed">8dp</dimen>
|
||||
<dimen name="toolbar_elevation">4dp</dimen>
|
||||
|
||||
</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,74 +1,90 @@
|
|||
<resources>
|
||||
|
||||
<string name="app_name">Nock Nock</string>
|
||||
<string name="app_name_x">Nock Nock %1$s</string>
|
||||
|
||||
<string name="no_sites_added">No sites added!</string>
|
||||
|
||||
<string name="about">About</string>
|
||||
<string name="about_body"><![CDATA[
|
||||
<b>Nock Nock</b>, a simple app designed by <b>Aidan Follestad</b>.<br/>
|
||||
<a href=\'https://aidanfollestad.com\'>Website</a>
|
||||
A simple app designed by <b>Aidan Follestad</b>.<br/>
|
||||
<a href=\'https://af.codes\'>Website</a>
|
||||
<a href=\'https://twitter.com/afollestad\'>Twitter</a>
|
||||
<a href=\'https://github.com/afollestad\'>GitHub</a>
|
||||
<a href=\'https://www.linkedin.com/in/afollestad\'>LinkedIn</a>
|
||||
<br/><br/><i>Nock Nock is open source! Check out the <a href=\'https://github.com/afollestad/nock-nock\'>GitHub page</a>!</i>
|
||||
<br/>Icon by <a href=\'https://plus.google.com/+KevinAguilarC\'>Kevin Aguilar</a> of <b>221 Pixels</b>.
|
||||
]]></string>
|
||||
<br/>View the <a href=\'https://af.codes/privacypolicies/nocknock.html\'>Privacy Policy</a>.
|
||||
]]></string>
|
||||
<string name="dark_mode">Dark Mode</string>
|
||||
|
||||
<string name="dismiss">Dismiss</string>
|
||||
<string name="add_site">Add Site</string>
|
||||
<string name="site_name">Site Name</string>
|
||||
<string name="site_name_hint">Site display name</string>
|
||||
<string name="site_url">Site URL</string>
|
||||
<string name="site_url_hint">https://yoursite.com</string>
|
||||
<string name="site_tags">Site Tags</string>
|
||||
<string name="site_tags_hint">e.g. One,Two,Three</string>
|
||||
<string name="site_tags_hint_full">Tags (e.g. One,Two,Three)</string>
|
||||
<string name="please_enter_name">Please enter a name!</string>
|
||||
<string name="please_enter_url">Please enter a URL.</string>
|
||||
<string name="please_enter_valid_url">Please enter a valid URL.</string>
|
||||
<string name="please_enter_check_interval">Please input a check interval.</string>
|
||||
<string name="please_enter_search_term">Please input a search term.</string>
|
||||
<string name="please_enter_javaScript">Please input a validation script.</string>
|
||||
<string name="please_enter_networkTimeout">Please enter a network timeout greater than 0.</string>
|
||||
<string name="please_enter_validCertUri">Certificate should be a valid file or content URI.</string>
|
||||
|
||||
<string name="options">Options</string>
|
||||
<string name="already_checking_sites">Already checking sites!</string>
|
||||
<string name="remove_site">Remove Site</string>
|
||||
<string name="duplicate_and_modify">Duplicate and Modify</string>
|
||||
<string name="remove_site_prompt"><![CDATA[Remove <b>%1$s</b> from your sites?]]></string>
|
||||
<string name="remove">Remove</string>
|
||||
<string name="save_changes">Save Changes</string>
|
||||
<string name="view_site">View Site</string>
|
||||
<string name="last_check_result">Last Check Result</string>
|
||||
<string name="next_check">Next Check</string>
|
||||
<string name="next_check_x">Next Check: %1$s</string>
|
||||
<string name="last_check_result">Last Validation Result</string>
|
||||
<string name="next_check">Next Validation</string>
|
||||
<string name="next_check_x">Next Validation: %1$s</string>
|
||||
<string name="now">Now</string>
|
||||
<string name="none_turned_off">None (turned off)</string>
|
||||
<string name="none">None</string>
|
||||
|
||||
<string name="disable_automatic_checks">Disable Automatic Checks</string>
|
||||
<string name="disable_automatic_checks">Disable Automatic Validation</string>
|
||||
<string name="disable_automatic_checks_prompt"><![CDATA[
|
||||
Disable automatic checks for <b>%1$s</b>? The site will not be validated in the background
|
||||
until you re-enable checks for it. You can still manually perform checks by tapping the
|
||||
Refresh icon at the top of this page.
|
||||
Disable automatic validation for <b>%1$s</b>? The site will not be checked in the background
|
||||
until you re-enable validation by tapping the checkmark (Save) icon. You can still manually
|
||||
perform validation by tapping the Refresh icon at the top of this page.
|
||||
]]></string>
|
||||
<string name="disable">Disable</string>
|
||||
<string name="renable_and_save_changes">Enable Checks & Save Changes</string>
|
||||
<string name="renable_and_save_changes">Enable Auto Validation & Save Changes</string>
|
||||
|
||||
<string name="response_timeout">Network Response Timeout (ms)</string>
|
||||
<string name="response_timeout_default">10000</string>
|
||||
|
||||
<string name="ssl_certificate">SSL Certificate</string>
|
||||
<string name="ssl_certificate_automatic">(Automatic)</string>
|
||||
<string name="ssl_certificate_browse">Browse</string>
|
||||
|
||||
<string name="refresh_status">Refresh Status</string>
|
||||
|
||||
<string name="warning_http_url">
|
||||
Warning: this app checks for server availability with HTTP requests. It\'s recommended that you
|
||||
Warning: this app validates sites availability with HTTP requests. It\'s recommended that you
|
||||
use an HTTP URL.
|
||||
</string>
|
||||
<string name="response_validation_mode">Response Validation Mode</string>
|
||||
<string name="search_term">Search term…</string>
|
||||
|
||||
<string name="validation_mode_status_desc">
|
||||
The HTTP status code is checked. If it\'s a successful status code, the site passes the check.
|
||||
The HTTP status code is checked. If it\'s a successful status code, the site passes validation.
|
||||
</string>
|
||||
<string name="validation_mode_term_desc">
|
||||
The status code check is done first. If it\'s successful, the response body is checked.
|
||||
If it contains your search term, the site passes the check.
|
||||
If it contains your search term, the site passes validation.
|
||||
</string>
|
||||
<string name="validation_mode_javascript_desc">
|
||||
The status code check is done first. If it\'s successful, the response body is passed to the
|
||||
JavaScript function above. If the function returns true, the site passes the check. Throw an
|
||||
JavaScript function above. If the function returns true, the site passes validation. Throw an
|
||||
exception to pass custom error messages to Nock Nock.
|
||||
</string>
|
||||
|
||||
<string name="install_web_browser">Please install a web browser app, such as Google Chrome.</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -1,73 +1,12 @@
|
|||
<resources>
|
||||
|
||||
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="colorButtonNormal">@color/colorPrimaryDark</item>
|
||||
<style name="AppTheme" parent="AppThemeParent"/>
|
||||
|
||||
<item name="android:listDivider">@drawable/divider</item>
|
||||
|
||||
<item name="android:textColorPrimary">#212121</item>
|
||||
<item name="android:textColorSecondary">#727272</item>
|
||||
|
||||
<item name="md_corner_radius">16dp</item>
|
||||
<item name="md_font_title">@font/lato_black</item>
|
||||
<item name="md_font_body">@font/lato</item>
|
||||
<item name="md_font_button">@font/lato_bold</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Ink" parent="Theme.MaterialComponents.NoActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="colorButtonNormal">@color/colorPrimaryDark</item>
|
||||
|
||||
<item name="android:listDivider">@drawable/divider</item>
|
||||
|
||||
<item name="md_corner_radius">16dp</item>
|
||||
<item name="md_font_title">@font/lato_black</item>
|
||||
<item name="md_font_body">@font/lato</item>
|
||||
<item name="md_font_button">@font/lato_bold</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Transparent" parent="AppTheme.Ink">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="MainToolbarTheme" parent="@style/Theme.MaterialComponents">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="android:fontFamily">@font/lato_black</item>
|
||||
</style>
|
||||
|
||||
<style name="MainToolbarStyle" parent="@style/Widget.MaterialComponents.Toolbar">
|
||||
<item name="android:background">?colorPrimary</item>
|
||||
<item name="android:elevation">@dimen/default_elevation</item>
|
||||
<item name="title">@string/app_name</item>
|
||||
<item name="titleTextColor">#FFFFFF</item>
|
||||
<item name="popupTheme">@style/Theme.MaterialComponents.Light.DarkActionBar</item>
|
||||
</style>
|
||||
|
||||
<style name="FlatToolbarTheme" parent="@style/Theme.MaterialComponents">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="android:fontFamily">@font/lato_black</item>
|
||||
</style>
|
||||
|
||||
<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="AppTheme.Dark" parent="AppThemeParent.Dark"/>
|
||||
|
||||
<style name="PrimaryDarkButton" parent="Widget.MaterialComponents.Button">
|
||||
<item name="android:textColor">#fff</item>
|
||||
<item name="backgroundTint">@color/colorPrimaryDark</item>
|
||||
<item name="backgroundTint">@color/darkerGray</item>
|
||||
<item name="android:fontFamily">@font/lato</item>
|
||||
</style>
|
||||
|
||||
|
|
54
app/src/main/res/values/styles_parents.xml
Normal file
|
@ -0,0 +1,54 @@
|
|||
<resources>
|
||||
|
||||
<style name="AppThemeParent" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary_lightTheme</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark_lightTheme</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="colorButtonNormal">@color/colorAccent</item>
|
||||
|
||||
<item name="toolbarTitleColor">#000000</item>
|
||||
<item name="dividerColor">#EEEEEE</item>
|
||||
<item name="iconColor">#000000</item>
|
||||
<item name="scriptLayoutBackground">@color/lighterGray</item>
|
||||
|
||||
<item name="android:textColorPrimary">#212121</item>
|
||||
<item name="android:textColorSecondary">#727272</item>
|
||||
|
||||
<item name="android:windowBackground">@color/colorPrimary_lightTheme</item>
|
||||
<item name="android:listDivider">@drawable/divider</item>
|
||||
|
||||
<item name="android:statusBarColor">#E5E5E5</item>
|
||||
|
||||
<item name="md_corner_radius">16dp</item>
|
||||
<item name="md_font_title">@font/lato_bold</item>
|
||||
<item name="md_font_body">@font/lato</item>
|
||||
<item name="md_font_button">@font/lato_bold</item>
|
||||
</style>
|
||||
|
||||
<style name="AppThemeParent.Dark" parent="Theme.MaterialComponents.NoActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary_darkTheme</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark_darkTheme</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="colorButtonNormal">@color/colorAccent</item>
|
||||
|
||||
<item name="toolbarTitleColor">#ffffff</item>
|
||||
<item name="dividerColor">#303030</item>
|
||||
<item name="iconColor">#FFFFFF</item>
|
||||
<item name="scriptLayoutBackground">@color/darkerGray</item>
|
||||
|
||||
<item name="android:textColorPrimary">#FFFFFF</item>
|
||||
<item name="android:textColorSecondary">#F0F0F0</item>
|
||||
|
||||
<item name="android:windowBackground">@color/colorPrimary_darkTheme</item>
|
||||
<item name="android:listDivider">@drawable/divider</item>
|
||||
|
||||
<item name="android:statusBarColor">@color/colorPrimary_darkTheme</item>
|
||||
<item name="android:navigationBarColor">@color/colorPrimaryDark_darkTheme</item>
|
||||
|
||||
<item name="md_corner_radius">16dp</item>
|
||||
<item name="md_font_title">@font/lato_bold</item>
|
||||
<item name="md_font_body">@font/lato</item>
|
||||
<item name="md_font_button">@font/lato_bold</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
33
app/src/main/res/values/styles_text.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<resources>
|
||||
|
||||
<style name="AppTheme.TextAppearance.Title" parent="TextAppearance.MaterialComponents.Headline6">
|
||||
<item name="fontFamily">@font/lato_bold</item>
|
||||
<item name="android:fontFamily">@font/lato_bold</item>
|
||||
<item name="android:textColor">?toolbarTitleColor</item>
|
||||
</style>
|
||||
|
||||
<style name="InputForm"/>
|
||||
|
||||
<style name="InputForm.Header" parent="NockText.SectionHeader">
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_marginTop">@dimen/content_inset_less</item>
|
||||
</style>
|
||||
|
||||
<style name="InputForm.Field" parent="NockText.Body">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_marginTop">@dimen/content_inset_quarter</item>
|
||||
<item name="android:singleLine">true</item>
|
||||
<item name="android:imeOptions">actionNext</item>
|
||||
<item name="android:layout_marginStart">-4dp</item>
|
||||
<item name="android:layout_marginEnd">-4dp</item>
|
||||
</style>
|
||||
|
||||
<style name="InputForm.FieldNote" parent="NockText.Footnote">
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_marginTop">@dimen/list_text_spacing</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
|
@ -1,241 +0,0 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock
|
||||
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.ui.addsite.AddSiteView
|
||||
import com.afollestad.nocknock.ui.addsite.InputErrors
|
||||
import com.afollestad.nocknock.ui.addsite.RealAddSitePresenter
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import com.nhaarman.mockitokotlin2.argumentCaptor
|
||||
import com.nhaarman.mockitokotlin2.doAnswer
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import com.nhaarman.mockitokotlin2.never
|
||||
import com.nhaarman.mockitokotlin2.times
|
||||
import com.nhaarman.mockitokotlin2.verify
|
||||
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
|
||||
import com.nhaarman.mockitokotlin2.whenever
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class AddSitePresenterTest {
|
||||
|
||||
private val serverModelStore = mock<ServerModelStore> {
|
||||
on { runBlocking { put(any()) } } doAnswer { inv ->
|
||||
inv.getArgument<ServerModel>(0)
|
||||
}
|
||||
}
|
||||
private val checkStatusManager = mock<CheckStatusManager>()
|
||||
private val view = mock<AddSiteView>()
|
||||
|
||||
private val presenter = RealAddSitePresenter(
|
||||
serverModelStore,
|
||||
checkStatusManager
|
||||
)
|
||||
|
||||
@Before fun setup() {
|
||||
doAnswer {
|
||||
val exec = it.getArgument<ScopeReceiver>(1)
|
||||
runBlocking { exec() }
|
||||
Unit
|
||||
}.whenever(view)
|
||||
.scopeWhileAttached(any(), any())
|
||||
|
||||
presenter.takeView(view)
|
||||
}
|
||||
|
||||
@After fun destroy() {
|
||||
presenter.dropView()
|
||||
}
|
||||
|
||||
@Test fun onUrlInputFocusChange_focused() {
|
||||
presenter.onUrlInputFocusChange(true, "hello")
|
||||
verifyNoMoreInteractions(view)
|
||||
}
|
||||
|
||||
@Test fun onUrlInputFocusChange_empty() {
|
||||
presenter.onUrlInputFocusChange(false, "")
|
||||
verifyNoMoreInteractions(view)
|
||||
}
|
||||
|
||||
@Test fun onUrlInputFocusChange_notHttpHttps() {
|
||||
presenter.onUrlInputFocusChange(false, "ftp://hello.com")
|
||||
verify(view).showOrHideUrlSchemeWarning(true)
|
||||
}
|
||||
|
||||
@Test fun onUrlInputFocusChange_isHttpOrHttps() {
|
||||
presenter.onUrlInputFocusChange(false, "http://hello.com")
|
||||
presenter.onUrlInputFocusChange(false, "https://hello.com")
|
||||
verify(view, times(2)).showOrHideUrlSchemeWarning(false)
|
||||
}
|
||||
|
||||
@Test fun onValidationModeSelected_statusCode() {
|
||||
presenter.onValidationModeSelected(0)
|
||||
verify(view).showOrHideValidationSearchTerm(false)
|
||||
verify(view).showOrHideScriptInput(false)
|
||||
verify(view).setValidationModeDescription(R.string.validation_mode_status_desc)
|
||||
}
|
||||
|
||||
@Test fun onValidationModeSelected_termSearch() {
|
||||
presenter.onValidationModeSelected(1)
|
||||
verify(view).showOrHideValidationSearchTerm(true)
|
||||
verify(view).showOrHideScriptInput(false)
|
||||
verify(view).setValidationModeDescription(R.string.validation_mode_term_desc)
|
||||
}
|
||||
|
||||
@Test fun onValidationModeSelected_javaScript() {
|
||||
presenter.onValidationModeSelected(2)
|
||||
verify(view).showOrHideValidationSearchTerm(false)
|
||||
verify(view).showOrHideScriptInput(true)
|
||||
verify(view).setValidationModeDescription(R.string.validation_mode_javascript_desc)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun onValidationModeSelected_other() {
|
||||
presenter.onValidationModeSelected(3)
|
||||
}
|
||||
|
||||
@Test fun commit_nameError() {
|
||||
presenter.commit(
|
||||
"",
|
||||
"https://test.com",
|
||||
1,
|
||||
STATUS_CODE,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.name).isEqualTo(R.string.please_enter_name)
|
||||
}
|
||||
|
||||
@Test fun commit_urlEmptyError() {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"",
|
||||
1,
|
||||
STATUS_CODE,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.url).isEqualTo(R.string.please_enter_url)
|
||||
}
|
||||
|
||||
@Test fun commit_urlFormatError() {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"ftp://hello.com",
|
||||
1,
|
||||
STATUS_CODE,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.url).isEqualTo(R.string.please_enter_valid_url)
|
||||
}
|
||||
|
||||
@Test fun commit_checkIntervalError() {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"https://hello.com",
|
||||
-1,
|
||||
STATUS_CODE,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.checkInterval).isEqualTo(R.string.please_enter_check_interval)
|
||||
}
|
||||
|
||||
@Test fun commit_termSearchError() {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"https://hello.com",
|
||||
1,
|
||||
TERM_SEARCH,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.termSearch).isEqualTo(R.string.please_enter_search_term)
|
||||
}
|
||||
|
||||
@Test fun commit_javaScript_error() {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"https://hello.com",
|
||||
1,
|
||||
JAVASCRIPT,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.javaScript).isEqualTo(R.string.please_enter_javaScript)
|
||||
}
|
||||
|
||||
@Test fun commit_success() = runBlocking {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"https://hello.com",
|
||||
1,
|
||||
STATUS_CODE,
|
||||
null
|
||||
)
|
||||
|
||||
val modelCaptor = argumentCaptor<ServerModel>()
|
||||
verify(view).setLoading()
|
||||
verify(serverModelStore).put(modelCaptor.capture())
|
||||
val model = modelCaptor.firstValue
|
||||
verify(view, never()).setInputErrors(any())
|
||||
verify(checkStatusManager).scheduleCheck(
|
||||
site = model,
|
||||
rightNow = true,
|
||||
cancelPrevious = true,
|
||||
fromFinishingJob = false
|
||||
)
|
||||
verify(view).setDoneLoading()
|
||||
verify(view).onSiteAdded()
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock
|
||||
|
||||
import android.content.Intent
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.ui.main.MainView
|
||||
import com.afollestad.nocknock.ui.main.RealMainPresenter
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import com.nhaarman.mockitokotlin2.argumentCaptor
|
||||
import com.nhaarman.mockitokotlin2.doAnswer
|
||||
import com.nhaarman.mockitokotlin2.doReturn
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import com.nhaarman.mockitokotlin2.times
|
||||
import com.nhaarman.mockitokotlin2.verify
|
||||
import com.nhaarman.mockitokotlin2.whenever
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class MainPresenterTest {
|
||||
|
||||
private val serverModelStore = mock<ServerModelStore>()
|
||||
private val notificationManager = mock<NockNotificationManager>()
|
||||
private val checkStatusManager = mock<CheckStatusManager>()
|
||||
private val view = mock<MainView>()
|
||||
|
||||
private val presenter = RealMainPresenter(
|
||||
serverModelStore,
|
||||
notificationManager,
|
||||
checkStatusManager
|
||||
)
|
||||
|
||||
@Before fun setup() {
|
||||
doAnswer {
|
||||
val exec = it.getArgument<ScopeReceiver>(1)
|
||||
runBlocking { exec() }
|
||||
Unit
|
||||
}.whenever(view)
|
||||
.scopeWhileAttached(any(), any())
|
||||
|
||||
presenter.takeView(view)
|
||||
}
|
||||
|
||||
@After fun destroy() {
|
||||
presenter.dropView()
|
||||
}
|
||||
|
||||
@Test fun onBroadcast() {
|
||||
val badIntent = fakeIntent("Hello World")
|
||||
presenter.onBroadcast(badIntent)
|
||||
|
||||
val model = fakeModel()
|
||||
val goodIntent = fakeIntent(ACTION_STATUS_UPDATE)
|
||||
whenever(goodIntent.getSerializableExtra(KEY_UPDATE_MODEL))
|
||||
.doReturn(model)
|
||||
|
||||
presenter.onBroadcast(goodIntent)
|
||||
verify(view, times(1)).updateModel(model)
|
||||
}
|
||||
|
||||
@Test fun resume() = runBlocking {
|
||||
val model = fakeModel()
|
||||
whenever(serverModelStore.get()).doReturn(listOf(model))
|
||||
presenter.resume()
|
||||
|
||||
verify(notificationManager).cancelStatusNotifications()
|
||||
|
||||
val modelsCaptor = argumentCaptor<List<ServerModel>>()
|
||||
verify(view, times(2)).setModels(modelsCaptor.capture())
|
||||
assertThat(modelsCaptor.firstValue).isEmpty()
|
||||
assertThat(modelsCaptor.lastValue.single()).isEqualTo(model)
|
||||
}
|
||||
|
||||
@Test fun refreshSite() {
|
||||
val model = fakeModel()
|
||||
presenter.refreshSite(model)
|
||||
|
||||
verify(checkStatusManager).scheduleCheck(
|
||||
site = model,
|
||||
rightNow = true,
|
||||
cancelPrevious = true
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun removeSite() = runBlocking {
|
||||
val model = fakeModel()
|
||||
presenter.removeSite(model)
|
||||
|
||||
verify(checkStatusManager).cancelCheck(model)
|
||||
verify(notificationManager).cancelStatusNotification(model)
|
||||
verify(serverModelStore).delete(model)
|
||||
verify(view).onSiteDeleted(model)
|
||||
}
|
||||
|
||||
private fun fakeModel() = ServerModel(
|
||||
name = "Test",
|
||||
url = "https://test.com",
|
||||
validationMode = STATUS_CODE
|
||||
)
|
||||
|
||||
private fun fakeIntent(action: String): Intent {
|
||||
return mock {
|
||||
on { getAction() } doReturn action
|
||||
}
|
||||
}
|
||||
}
|
216
app/src/test/java/com/afollestad/nocknock/TestData.kt
Normal file
|
@ -0,0 +1,216 @@
|
|||
/**
|
||||
* 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
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import com.afollestad.nocknock.data.AppDatabase
|
||||
import com.afollestad.nocknock.data.HeaderDao
|
||||
import com.afollestad.nocknock.data.RetryPolicyDao
|
||||
import com.afollestad.nocknock.data.SiteDao
|
||||
import com.afollestad.nocknock.data.SiteSettingsDao
|
||||
import com.afollestad.nocknock.data.ValidationResultsDao
|
||||
import com.afollestad.nocknock.data.model.Header
|
||||
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.SiteSettings
|
||||
import com.afollestad.nocknock.data.model.Status
|
||||
import com.afollestad.nocknock.data.model.Status.OK
|
||||
import com.afollestad.nocknock.data.model.ValidationMode
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.model.ValidationResult
|
||||
import com.afollestad.nocknock.utilities.providers.CanNotifyModel
|
||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import com.nhaarman.mockitokotlin2.doAnswer
|
||||
import com.nhaarman.mockitokotlin2.doReturn
|
||||
import com.nhaarman.mockitokotlin2.isA
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import java.lang.System.currentTimeMillis
|
||||
|
||||
fun fakeIntent(action: String): Intent {
|
||||
return mock {
|
||||
on { getAction() } doReturn action
|
||||
}
|
||||
}
|
||||
|
||||
fun fakeSettingsModel(
|
||||
id: Long,
|
||||
validationMode: ValidationMode = STATUS_CODE
|
||||
) = SiteSettings(
|
||||
siteId = id,
|
||||
validationIntervalMs = 600000,
|
||||
validationMode = validationMode,
|
||||
validationArgs = null,
|
||||
disabled = false,
|
||||
networkTimeout = 10000,
|
||||
certificate = null
|
||||
)
|
||||
|
||||
fun fakeResultModel(
|
||||
id: Long,
|
||||
status: Status = OK,
|
||||
reason: String? = null
|
||||
) = ValidationResult(
|
||||
siteId = id,
|
||||
status = status,
|
||||
reason = reason,
|
||||
timestampMs = currentTimeMillis()
|
||||
)
|
||||
|
||||
fun fakeRetryPolicy(
|
||||
id: Long,
|
||||
count: Int = 3,
|
||||
minutes: Int = 6
|
||||
) = RetryPolicy(
|
||||
siteId = id,
|
||||
count = count,
|
||||
minutes = minutes
|
||||
)
|
||||
|
||||
fun fakeHeaders(siteId: Long): List<Header> {
|
||||
return listOf(
|
||||
Header(id = siteId + 1, siteId = siteId, key = "Content-Type", value = "text/html"),
|
||||
Header(id = siteId + 2, siteId = siteId, key = "User-Agent", value = "NockNock")
|
||||
)
|
||||
}
|
||||
|
||||
fun fakeModel(
|
||||
id: Long,
|
||||
tags: String = ""
|
||||
) = Site(
|
||||
id = id,
|
||||
name = "Test",
|
||||
url = "https://test.com",
|
||||
tags = tags,
|
||||
settings = fakeSettingsModel(id),
|
||||
lastResult = fakeResultModel(id),
|
||||
retryPolicy = fakeRetryPolicy(id),
|
||||
headers = fakeHeaders(id)
|
||||
)
|
||||
|
||||
val MOCK_MODEL_1 = fakeModel(1, tags = "one,two")
|
||||
val MOCK_MODEL_2 = fakeModel(2, tags = "three,four")
|
||||
val MOCK_MODEL_3 = fakeModel(3, tags = "five,six")
|
||||
|
||||
val ALL_MOCK_MODELS = listOf(MOCK_MODEL_1, MOCK_MODEL_2, MOCK_MODEL_3)
|
||||
|
||||
fun mockDatabase(): AppDatabase {
|
||||
val siteDao = mock<SiteDao> {
|
||||
on { insert(isA()) } doReturn 1
|
||||
on { one(isA()) } doAnswer { inv ->
|
||||
val id = inv.getArgument<Long>(0)
|
||||
return@doAnswer when (id) {
|
||||
1L -> listOf(MOCK_MODEL_1)
|
||||
2L -> listOf(MOCK_MODEL_2)
|
||||
3L -> listOf(MOCK_MODEL_3)
|
||||
else -> listOf()
|
||||
}
|
||||
}
|
||||
on { all() } doReturn ALL_MOCK_MODELS
|
||||
on { update(isA()) } doAnswer { inv ->
|
||||
return@doAnswer inv.arguments.size
|
||||
}
|
||||
on { delete(isA()) } doAnswer { inv ->
|
||||
return@doAnswer inv.arguments.size
|
||||
}
|
||||
}
|
||||
val settingsDao = mock<SiteSettingsDao> {
|
||||
on { insert(isA()) } doReturn 1L
|
||||
on { forSite(isA()) } doAnswer { inv ->
|
||||
val id = inv.getArgument<Long>(0)
|
||||
return@doAnswer when (id) {
|
||||
1L -> listOf(MOCK_MODEL_1.settings!!)
|
||||
2L -> listOf(MOCK_MODEL_2.settings!!)
|
||||
3L -> listOf(MOCK_MODEL_3.settings!!)
|
||||
else -> listOf()
|
||||
}
|
||||
}
|
||||
on { update(isA()) } doReturn 1
|
||||
on { delete(isA()) } doReturn 1
|
||||
}
|
||||
val resultsDao = mock<ValidationResultsDao> {
|
||||
on { insert(isA()) } doReturn 1L
|
||||
on { forSite(isA()) } doAnswer { inv ->
|
||||
val id = inv.getArgument<Long>(0)
|
||||
return@doAnswer when (id) {
|
||||
1L -> listOf(MOCK_MODEL_1.lastResult!!)
|
||||
2L -> listOf(MOCK_MODEL_2.lastResult!!)
|
||||
3L -> listOf(MOCK_MODEL_3.lastResult!!)
|
||||
else -> listOf()
|
||||
}
|
||||
}
|
||||
on { update(isA()) } doReturn 1
|
||||
on { delete(isA()) } doReturn 1
|
||||
}
|
||||
val retryDao = mock<RetryPolicyDao> {
|
||||
on { insert(isA()) } doReturn 1L
|
||||
on { forSite(isA()) } doAnswer { inv ->
|
||||
val id = inv.getArgument<Long>(0)
|
||||
return@doAnswer when (id) {
|
||||
1L -> listOf(MOCK_MODEL_1.retryPolicy!!)
|
||||
2L -> listOf(MOCK_MODEL_2.retryPolicy!!)
|
||||
3L -> listOf(MOCK_MODEL_3.retryPolicy!!)
|
||||
else -> listOf()
|
||||
}
|
||||
}
|
||||
on { update(isA()) } doReturn 1
|
||||
on { delete(isA()) } doReturn 1
|
||||
}
|
||||
val headerDao = mock<HeaderDao> {
|
||||
on { all() } doReturn MOCK_MODEL_1.headers + MOCK_MODEL_2.headers + MOCK_MODEL_3.headers
|
||||
on { forSite(isA()) } doAnswer { inv ->
|
||||
val id = inv.getArgument<Long>(0)
|
||||
return@doAnswer when (id) {
|
||||
1L -> MOCK_MODEL_1.headers
|
||||
2L -> MOCK_MODEL_2.headers
|
||||
3L -> MOCK_MODEL_3.headers
|
||||
else -> listOf()
|
||||
}
|
||||
}
|
||||
on { insert(isA<Header>()) } doReturn 1L
|
||||
on { insert(isA<List<Header>>()) } doReturn listOf(1L, 2L)
|
||||
on { update(isA()) } doReturn 1
|
||||
on { delete(isA()) } doReturn 1
|
||||
}
|
||||
|
||||
return mock {
|
||||
on { siteDao() } doReturn siteDao
|
||||
on { siteSettingsDao() } doReturn settingsDao
|
||||
on { validationResultsDao() } doReturn resultsDao
|
||||
on { retryPolicyDao() } doReturn retryDao
|
||||
on { headerDao() } doReturn headerDao
|
||||
}
|
||||
}
|
||||
|
||||
fun mockIntentProvider() = object : IntentProvider {
|
||||
override fun createFilter(vararg actions: String): IntentFilter {
|
||||
return mock {
|
||||
on { this.getAction(any()) } doAnswer { inv ->
|
||||
val index = inv.getArgument<Int>(0)
|
||||
return@doAnswer actions[index]
|
||||
}
|
||||
on { this.actionsIterator() } doReturn actions.iterator()
|
||||
on { this.countActions() } doReturn actions.size
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPendingIntentForViewSite(model: CanNotifyModel): PendingIntent {
|
||||
// basically no-op right now
|
||||
return mock()
|
||||
}
|
||||
}
|
|
@ -1,361 +0,0 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock
|
||||
|
||||
import android.content.Intent
|
||||
import com.afollestad.nocknock.data.LAST_CHECK_NONE
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.ui.viewsite.InputErrors
|
||||
import com.afollestad.nocknock.ui.viewsite.KEY_VIEW_MODEL
|
||||
import com.afollestad.nocknock.ui.viewsite.RealViewSitePresenter
|
||||
import com.afollestad.nocknock.ui.viewsite.ViewSiteView
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import com.nhaarman.mockitokotlin2.argumentCaptor
|
||||
import com.nhaarman.mockitokotlin2.doAnswer
|
||||
import com.nhaarman.mockitokotlin2.doReturn
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import com.nhaarman.mockitokotlin2.never
|
||||
import com.nhaarman.mockitokotlin2.times
|
||||
import com.nhaarman.mockitokotlin2.verify
|
||||
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
|
||||
import com.nhaarman.mockitokotlin2.whenever
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class ViewSitePresenterTest {
|
||||
|
||||
private val serverModelStore = mock<ServerModelStore> {
|
||||
on { runBlocking { put(any()) } } doAnswer { inv ->
|
||||
inv.getArgument<ServerModel>(0)
|
||||
}
|
||||
}
|
||||
private val checkStatusManager = mock<CheckStatusManager>()
|
||||
private val notificationManager = mock<NockNotificationManager>()
|
||||
private val view = mock<ViewSiteView>()
|
||||
|
||||
private val presenter = RealViewSitePresenter(
|
||||
serverModelStore,
|
||||
checkStatusManager,
|
||||
notificationManager
|
||||
)
|
||||
|
||||
@Before fun setup() {
|
||||
doAnswer {
|
||||
val exec = it.getArgument<ScopeReceiver>(1)
|
||||
runBlocking { exec() }
|
||||
Unit
|
||||
}.whenever(view)
|
||||
.scopeWhileAttached(any(), any())
|
||||
|
||||
val model = fakeModel().copy(lastCheck = 0)
|
||||
val intent = fakeIntent("")
|
||||
whenever(intent.getSerializableExtra(KEY_VIEW_MODEL))
|
||||
.doReturn(model)
|
||||
presenter.takeView(view, intent)
|
||||
assertThat(presenter.currentModel()).isEqualTo(model)
|
||||
verify(view, times(1)).displayModel(model)
|
||||
}
|
||||
|
||||
@After fun destroy() {
|
||||
presenter.dropView()
|
||||
}
|
||||
|
||||
@Test fun onBroadcast() {
|
||||
val badIntent = fakeIntent("Hello World")
|
||||
presenter.onBroadcast(badIntent)
|
||||
|
||||
val model = fakeModel()
|
||||
val goodIntent = fakeIntent(ACTION_STATUS_UPDATE)
|
||||
whenever(goodIntent.getSerializableExtra(KEY_UPDATE_MODEL))
|
||||
.doReturn(model)
|
||||
|
||||
presenter.onBroadcast(goodIntent)
|
||||
assertThat(presenter.currentModel()).isEqualTo(model)
|
||||
verify(view, times(1)).displayModel(model)
|
||||
}
|
||||
|
||||
@Test fun onNewIntent() {
|
||||
val badIntent = fakeIntent(ACTION_STATUS_UPDATE)
|
||||
presenter.onBroadcast(badIntent)
|
||||
|
||||
val model = fakeModel()
|
||||
val goodIntent = fakeIntent(ACTION_STATUS_UPDATE)
|
||||
whenever(goodIntent.getSerializableExtra(KEY_VIEW_MODEL))
|
||||
.doReturn(model)
|
||||
presenter.onBroadcast(goodIntent)
|
||||
|
||||
verify(view, times(1)).displayModel(model)
|
||||
}
|
||||
|
||||
@Test fun onUrlInputFocusChange_focused() {
|
||||
presenter.onUrlInputFocusChange(true, "hello")
|
||||
verifyNoMoreInteractions(view)
|
||||
}
|
||||
|
||||
@Test fun onUrlInputFocusChange_empty() {
|
||||
presenter.onUrlInputFocusChange(false, "")
|
||||
verifyNoMoreInteractions(view)
|
||||
}
|
||||
|
||||
@Test fun onUrlInputFocusChange_notHttpHttps() {
|
||||
presenter.onUrlInputFocusChange(false, "ftp://hello.com")
|
||||
verify(view).showOrHideUrlSchemeWarning(true)
|
||||
}
|
||||
|
||||
@Test fun onUrlInputFocusChange_isHttpOrHttps() {
|
||||
presenter.onUrlInputFocusChange(false, "http://hello.com")
|
||||
presenter.onUrlInputFocusChange(false, "https://hello.com")
|
||||
verify(view, times(2)).showOrHideUrlSchemeWarning(false)
|
||||
}
|
||||
|
||||
@Test fun onValidationModeSelected_statusCode() {
|
||||
presenter.onValidationModeSelected(0)
|
||||
verify(view).showOrHideValidationSearchTerm(false)
|
||||
verify(view).showOrHideScriptInput(false)
|
||||
verify(view).setValidationModeDescription(R.string.validation_mode_status_desc)
|
||||
}
|
||||
|
||||
@Test fun onValidationModeSelected_termSearch() {
|
||||
presenter.onValidationModeSelected(1)
|
||||
verify(view).showOrHideValidationSearchTerm(true)
|
||||
verify(view).showOrHideScriptInput(false)
|
||||
verify(view).setValidationModeDescription(R.string.validation_mode_term_desc)
|
||||
}
|
||||
|
||||
@Test fun onValidationModeSelected_javaScript() {
|
||||
presenter.onValidationModeSelected(2)
|
||||
verify(view).showOrHideValidationSearchTerm(false)
|
||||
verify(view).showOrHideScriptInput(true)
|
||||
verify(view).setValidationModeDescription(R.string.validation_mode_javascript_desc)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun onValidationModeSelected_other() {
|
||||
presenter.onValidationModeSelected(3)
|
||||
}
|
||||
|
||||
@Test fun commit_nameError() {
|
||||
presenter.commit(
|
||||
"",
|
||||
"https://test.com",
|
||||
1,
|
||||
STATUS_CODE,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.name).isEqualTo(R.string.please_enter_name)
|
||||
}
|
||||
|
||||
@Test fun commit_urlEmptyError() {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"",
|
||||
1,
|
||||
STATUS_CODE,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.url).isEqualTo(R.string.please_enter_url)
|
||||
}
|
||||
|
||||
@Test fun commit_urlFormatError() {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"ftp://hello.com",
|
||||
1,
|
||||
STATUS_CODE,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.url).isEqualTo(R.string.please_enter_valid_url)
|
||||
}
|
||||
|
||||
@Test fun commit_checkIntervalError() {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"https://hello.com",
|
||||
-1,
|
||||
STATUS_CODE,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.checkInterval).isEqualTo(R.string.please_enter_check_interval)
|
||||
}
|
||||
|
||||
@Test fun commit_termSearchError() {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"https://hello.com",
|
||||
1,
|
||||
TERM_SEARCH,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.termSearch).isEqualTo(R.string.please_enter_search_term)
|
||||
}
|
||||
|
||||
@Test fun commit_javaScript_error() {
|
||||
presenter.commit(
|
||||
"Testing",
|
||||
"https://hello.com",
|
||||
1,
|
||||
JAVASCRIPT,
|
||||
null
|
||||
)
|
||||
|
||||
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||
verify(checkStatusManager, never())
|
||||
.scheduleCheck(any(), any(), any(), any())
|
||||
|
||||
val errors = inputErrorsCaptor.firstValue
|
||||
assertThat(errors.javaScript).isEqualTo(R.string.please_enter_javaScript)
|
||||
}
|
||||
|
||||
@Test fun commit_success() = runBlocking {
|
||||
val name = "Testing"
|
||||
val url = "https://hello.com"
|
||||
val checkInterval = 60000L
|
||||
val validationMode = TERM_SEARCH
|
||||
val validationContent = "Hello World"
|
||||
|
||||
val disabledModel = presenter.currentModel()
|
||||
.copy(disabled = true)
|
||||
presenter.setModel(disabledModel)
|
||||
|
||||
presenter.commit(
|
||||
name,
|
||||
url,
|
||||
checkInterval,
|
||||
validationMode,
|
||||
validationContent
|
||||
)
|
||||
|
||||
val modelCaptor = argumentCaptor<ServerModel>()
|
||||
verify(view).setLoading()
|
||||
verify(serverModelStore).update(modelCaptor.capture())
|
||||
|
||||
val model = modelCaptor.firstValue
|
||||
assertThat(model.name).isEqualTo(name)
|
||||
assertThat(model.url).isEqualTo(url)
|
||||
assertThat(model.checkInterval).isEqualTo(checkInterval)
|
||||
assertThat(model.validationMode).isEqualTo(validationMode)
|
||||
assertThat(model.validationContent).isEqualTo(validationContent)
|
||||
assertThat(model.disabled).isFalse()
|
||||
|
||||
verify(view, never()).setInputErrors(any())
|
||||
verify(checkStatusManager).scheduleCheck(
|
||||
site = model,
|
||||
rightNow = true,
|
||||
cancelPrevious = true,
|
||||
fromFinishingJob = false
|
||||
)
|
||||
verify(view).setDoneLoading()
|
||||
verify(view).finish()
|
||||
}
|
||||
|
||||
@Test fun checkNow() {
|
||||
val newModel = presenter.currentModel()
|
||||
.copy(
|
||||
status = WAITING
|
||||
)
|
||||
presenter.checkNow()
|
||||
|
||||
verify(view, never()).setLoading()
|
||||
verify(view).displayModel(newModel)
|
||||
verify(checkStatusManager).scheduleCheck(
|
||||
site = newModel,
|
||||
rightNow = true,
|
||||
cancelPrevious = true
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun disableChecks() = runBlocking {
|
||||
val model = presenter.currentModel()
|
||||
presenter.disableChecks()
|
||||
|
||||
verify(checkStatusManager).cancelCheck(model)
|
||||
verify(notificationManager).cancelStatusNotification(model)
|
||||
verify(view).setLoading()
|
||||
|
||||
val modelCaptor = argumentCaptor<ServerModel>()
|
||||
verify(serverModelStore).update(modelCaptor.capture())
|
||||
val newModel = modelCaptor.firstValue
|
||||
assertThat(newModel.disabled).isTrue()
|
||||
assertThat(newModel.lastCheck).isEqualTo(LAST_CHECK_NONE)
|
||||
|
||||
verify(view).setDoneLoading()
|
||||
verify(view, times(1)).displayModel(newModel)
|
||||
}
|
||||
|
||||
@Test fun removeSite() = runBlocking {
|
||||
val model = presenter.currentModel()
|
||||
presenter.removeSite()
|
||||
|
||||
verify(checkStatusManager).cancelCheck(model)
|
||||
verify(notificationManager).cancelStatusNotification(model)
|
||||
verify(view).setLoading()
|
||||
verify(serverModelStore).delete(model)
|
||||
verify(view).setDoneLoading()
|
||||
verify(view).finish()
|
||||
}
|
||||
|
||||
private fun fakeModel() = ServerModel(
|
||||
id = 1,
|
||||
name = "Test",
|
||||
url = "https://test.com",
|
||||
validationMode = STATUS_CODE
|
||||
)
|
||||
|
||||
private fun fakeIntent(action: String): Intent {
|
||||
return mock {
|
||||
on { getAction() } doReturn action
|
||||
}
|
||||
}
|
||||
}
|