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,7 +1,6 @@
|
||||||
|
|
||||||
### Guidelines
|
### Guidelines
|
||||||
|
|
||||||
1. You must run the `spotlessApply` task before commiting, either through Android Studio or with `./gradlew spotlessApply`.
|
1. You must run the `spotlessApply` task before committing, either through Android Studio or with `./gradlew spotlessApply`.
|
||||||
2. A PR should be focused and contained. If you are changing multiple unrelated things, they should be in separate PRs.
|
2. A PR should be focused and contained. If you are changing multiple unrelated things, they should be in separate PRs.
|
||||||
3. A PR should fix a bug or solve a problem - something that only you would use is not necessarily something that should be published.
|
3. A PR should fix a bug or solve a problem - something that only you would use is not necessarily something that should be published.
|
||||||
4. Give your PR a detailed title and description - look over your code one last time before actually creating the PR. Give it a self-review.
|
4. Give your PR a detailed title and description - look over your code one last time before actually creating the PR. Give it a self-review.
|
2
.gitignore
vendored
|
@ -181,3 +181,5 @@ gradle-app.setting
|
||||||
|
|
||||||
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||||
# gradle/wrapper/gradle-wrapper.properties
|
# gradle/wrapper/gradle-wrapper.properties
|
||||||
|
|
||||||
|
app/google-services.json
|
17
.idea/misc.xml
generated
|
@ -1,11 +1,16 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="CMakeSettings">
|
||||||
|
<configurations>
|
||||||
|
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
|
||||||
|
</configurations>
|
||||||
|
</component>
|
||||||
<component name="NullableNotNullManager">
|
<component name="NullableNotNullManager">
|
||||||
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
|
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
|
||||||
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
|
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
|
||||||
<option name="myNullables">
|
<option name="myNullables">
|
||||||
<value>
|
<value>
|
||||||
<list size="7">
|
<list size="10">
|
||||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
<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="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||||
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
|
<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="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
||||||
<item index="5" class="java.lang.String" itemvalue="androidx.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="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>
|
</list>
|
||||||
</value>
|
</value>
|
||||||
</option>
|
</option>
|
||||||
<option name="myNotNulls">
|
<option name="myNotNulls">
|
||||||
<value>
|
<value>
|
||||||
<list size="6">
|
<list size="9">
|
||||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
<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="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="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="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
||||||
<item index="4" class="java.lang.String" itemvalue="androidx.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="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>
|
</list>
|
||||||
</value>
|
</value>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</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" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|
2
.idea/modules.xml
generated
|
@ -3,11 +3,11 @@
|
||||||
<component name="ProjectModuleManager">
|
<component name="ProjectModuleManager">
|
||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
|
<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$/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$/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$/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$/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" />
|
<module fileurl="file://$PROJECT_DIR$/viewcomponents/viewcomponents.iml" filepath="$PROJECT_DIR$/viewcomponents/viewcomponents.iml" />
|
||||||
</modules>
|
</modules>
|
||||||
</component>
|
</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
|
## Nock Nock
|
||||||
|
|
||||||
[](https://travis-ci.org/afollestad/nock-nock)
|
|
||||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Nock Nock is a simple app which allows you to monitor your websites for maximum uptime.
|
Nock Nock is a simple app which allows you to monitor your websites for maximum uptime.
|
||||||
|
|
||||||
|
|
|
@ -15,30 +15,65 @@ android {
|
||||||
versionCode versions.publishVersionCode
|
versionCode versions.publishVersionCode
|
||||||
versionName versions.publishVersion
|
versionName versions.publishVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility 1.8
|
||||||
|
targetCompatibility 1.8
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
exclude 'META-INF/atomicfu.kotlin_module'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':data')
|
implementation project(':common')
|
||||||
implementation project(':utilities')
|
|
||||||
implementation project(':engine')
|
implementation project(':engine')
|
||||||
|
implementation project(':data')
|
||||||
implementation project(':notifications')
|
implementation project(':notifications')
|
||||||
implementation project(':viewcomponents')
|
implementation project(':viewcomponents')
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:' + versions.androidx
|
// Google/AppCompat
|
||||||
implementation 'androidx.recyclerview:recyclerview:' + versions.androidx
|
implementation 'androidx.appcompat:appcompat:' + versions.androidxCore
|
||||||
implementation 'com.google.android.material:material:' + versions.androidx
|
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 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin
|
||||||
|
|
||||||
implementation 'com.google.dagger:dagger:' + versions.dagger
|
// JOIN
|
||||||
kapt 'com.google.dagger:dagger-compiler:' + versions.dagger
|
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
|
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 'junit:junit:' + versions.junit
|
||||||
testImplementation 'org.mockito:mockito-core:' + versions.mockito
|
testImplementation 'org.mockito:mockito-core:' + versions.mockito
|
||||||
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:' + versions.mockitoKotlin
|
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:' + versions.mockitoKotlin
|
||||||
testImplementation 'com.google.truth:truth:' + versions.truth
|
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:name="com.afollestad.nocknock.ui.addsite.AddSiteActivity"
|
||||||
android:label="@string/add_site"
|
android:label="@string/add_site"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/AppTheme.Transparent"
|
|
||||||
android:windowSoftInputMode="stateHidden"/>
|
android:windowSoftInputMode="stateHidden"/>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.afollestad.nocknock.ui.viewsite.ViewSiteActivity"
|
android:name="com.afollestad.nocknock.ui.viewsite.ViewSiteActivity"
|
||||||
android:label="@string/view_site"
|
android:label="@string/view_site"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/AppTheme.Ink"
|
|
||||||
android:windowSoftInputMode="stateHidden"/>
|
android:windowSoftInputMode="stateHidden"/>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".engine.statuscheck.CheckStatusJob"
|
android:name=".engine.validation.ValidationJob"
|
||||||
android:label="@string/check_service_name"
|
android:label="@string/check_service_name"
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE"/>
|
android:permission="android.permission.BIND_JOB_SERVICE"/>
|
||||||
|
|
||||||
<receiver android:name=".engine.statuscheck.BootReceiver">
|
<receiver android:name=".engine.validation.BootReceiver">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
|
@ -1,16 +1,32 @@
|
||||||
/*
|
/**
|
||||||
* Licensed under Apache-2.0
|
|
||||||
*
|
|
||||||
* Designed and developed by Aidan Follestad (@afollestad)
|
* 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
|
package com.afollestad.nocknock
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.Application.ActivityLifecycleCallbacks
|
import android.app.Application.ActivityLifecycleCallbacks
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||||
import androidx.core.text.HtmlCompat.fromHtml
|
import androidx.core.text.HtmlCompat.fromHtml
|
||||||
|
import com.afollestad.materialdialogs.utils.MDUtil.resolveColor
|
||||||
|
import com.afollestad.nocknock.utilities.ext.toUri
|
||||||
|
import com.afollestad.nocknock.utilities.ui.toast
|
||||||
|
|
||||||
typealias ActivityLifeChange = (activity: Activity, resumed: Boolean) -> Unit
|
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 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)
|
* 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
|
package com.afollestad.nocknock
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
import com.afollestad.nocknock.BuildConfig.DEBUG
|
||||||
import com.afollestad.nocknock.di.AppComponent
|
import com.afollestad.nocknock.engine.engineModule
|
||||||
import com.afollestad.nocknock.di.DaggerAppComponent
|
import com.afollestad.nocknock.koin.mainModule
|
||||||
import com.afollestad.nocknock.engine.statuscheck.BootReceiver
|
import com.afollestad.nocknock.koin.prefModule
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob
|
import com.afollestad.nocknock.koin.viewModelModule
|
||||||
|
import com.afollestad.nocknock.logging.FabricTree
|
||||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
import com.afollestad.nocknock.notifications.notificationsModule
|
||||||
import com.afollestad.nocknock.ui.main.MainActivity
|
import com.afollestad.nocknock.utilities.commonModule
|
||||||
import com.afollestad.nocknock.ui.viewsite.ViewSiteActivity
|
import com.crashlytics.android.Crashlytics
|
||||||
import com.afollestad.nocknock.utilities.Injector
|
import io.fabric.sdk.android.Fabric
|
||||||
import com.afollestad.nocknock.utilities.ext.systemService
|
import org.koin.android.ext.android.inject
|
||||||
import okhttp3.OkHttpClient
|
import org.koin.android.ext.android.startKoin
|
||||||
import javax.inject.Inject
|
import timber.log.Timber
|
||||||
|
import timber.log.Timber.DebugTree
|
||||||
|
import timber.log.Timber.d as log
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class NockNockApp : Application(), Injector {
|
class NockNockApp : Application() {
|
||||||
|
|
||||||
companion object {
|
|
||||||
private fun log(message: String) {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.d("NockNockApp", message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var appComponent: AppComponent
|
|
||||||
@Inject lateinit var nockNotificationManager: NockNotificationManager
|
|
||||||
|
|
||||||
private var resumedActivities: Int = 0
|
private var resumedActivities: Int = 0
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
val okHttpClient = OkHttpClient.Builder()
|
if (DEBUG) {
|
||||||
.addNetworkInterceptor { chain ->
|
Timber.plant(DebugTree())
|
||||||
val request = chain.request()
|
}
|
||||||
.newBuilder()
|
|
||||||
.addHeader("User-Agent", "com.afollestad.nocknock")
|
|
||||||
.build()
|
|
||||||
chain.proceed(request)
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
|
|
||||||
appComponent = DaggerAppComponent.builder()
|
Timber.plant(FabricTree())
|
||||||
.application(this)
|
Fabric.with(this, Crashlytics())
|
||||||
.okHttpClient(okHttpClient)
|
|
||||||
.jobScheduler(systemService(JOB_SCHEDULER_SERVICE))
|
|
||||||
.notificationManager(systemService(NOTIFICATION_SERVICE))
|
|
||||||
.build()
|
|
||||||
appComponent.inject(this)
|
|
||||||
|
|
||||||
|
val modules = listOf(
|
||||||
|
prefModule,
|
||||||
|
mainModule,
|
||||||
|
engineModule,
|
||||||
|
commonModule,
|
||||||
|
notificationsModule,
|
||||||
|
viewModelModule
|
||||||
|
)
|
||||||
|
startKoin(
|
||||||
|
androidContext = this,
|
||||||
|
modules = modules
|
||||||
|
)
|
||||||
|
|
||||||
|
val nockNotificationManager by inject<NockNotificationManager>()
|
||||||
onActivityLifeChange { activity, resumed ->
|
onActivityLifeChange { activity, resumed ->
|
||||||
if (resumed) {
|
if (resumed) {
|
||||||
resumedActivities++
|
resumedActivities++
|
||||||
|
@ -69,13 +76,4 @@ class NockNockApp : Application(), Injector {
|
||||||
nockNotificationManager.setIsAppOpen(resumedActivities > 0)
|
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)
|
* 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
|
package com.afollestad.nocknock.adapter
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil.calculateDiff
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.isPending
|
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||||
import com.afollestad.nocknock.data.textRes
|
import com.afollestad.nocknock.data.model.isPending
|
||||||
|
import com.afollestad.nocknock.data.model.textRes
|
||||||
import com.afollestad.nocknock.utilities.ui.onDebouncedClick
|
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.iconStatus
|
||||||
import kotlinx.android.synthetic.main.list_item_server.view.textInterval
|
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.textStatus
|
||||||
import kotlinx.android.synthetic.main.list_item_server.view.textUrl
|
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) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class ServerVH constructor(
|
class SiteViewHolder constructor(
|
||||||
itemView: View,
|
itemView: View,
|
||||||
private val adapter: ServerAdapter
|
private val adapter: SiteAdapter
|
||||||
) : RecyclerView.ViewHolder(itemView), View.OnLongClickListener {
|
) : RecyclerView.ViewHolder(itemView), View.OnLongClickListener {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -35,24 +47,32 @@ class ServerVH constructor(
|
||||||
itemView.setOnLongClickListener(this)
|
itemView.setOnLongClickListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(model: ServerModel) {
|
fun bind(model: Site) {
|
||||||
|
requireNotNull(model.settings) { "Settings must be populated." }
|
||||||
|
|
||||||
itemView.textName.text = model.name
|
itemView.textName.text = model.name
|
||||||
itemView.textUrl.text = model.url
|
itemView.textUrl.text = model.url
|
||||||
itemView.iconStatus.setStatus(model.status)
|
|
||||||
|
|
||||||
val statusText = model.status.textRes()
|
val lastResult = model.lastResult
|
||||||
if (statusText == 0) {
|
if (lastResult != null) {
|
||||||
itemView.textStatus.text = model.reason
|
itemView.iconStatus.setStatus(lastResult.status)
|
||||||
|
val statusText = lastResult.status.textRes()
|
||||||
|
if (statusText == 0) {
|
||||||
|
itemView.textStatus.text = lastResult.reason
|
||||||
|
} else {
|
||||||
|
itemView.textStatus.setText(statusText)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
itemView.textStatus.setText(statusText)
|
itemView.iconStatus.setStatus(WAITING)
|
||||||
|
itemView.textStatus.setText(R.string.none)
|
||||||
}
|
}
|
||||||
|
|
||||||
val res = itemView.resources
|
val res = itemView.resources
|
||||||
when {
|
when {
|
||||||
model.disabled -> {
|
model.settings?.disabled == true -> {
|
||||||
itemView.textInterval.setText(R.string.checks_disabled)
|
itemView.textInterval.setText(R.string.checks_disabled)
|
||||||
}
|
}
|
||||||
model.status.isPending() -> {
|
model.lastResult?.status.isPending() -> {
|
||||||
itemView.textInterval.text = res.getString(
|
itemView.textInterval.text = res.getString(
|
||||||
R.string.next_check_x,
|
R.string.next_check_x,
|
||||||
res.getString(R.string.now)
|
res.getString(R.string.now)
|
||||||
|
@ -74,70 +94,33 @@ class ServerVH constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @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(
|
internal fun performClick(
|
||||||
index: Int,
|
index: Int,
|
||||||
longClick: Boolean
|
longClick: Boolean
|
||||||
) = listener.invoke(models[index], longClick)
|
) = listener.invoke(models[index], longClick)
|
||||||
|
|
||||||
fun add(model: ServerModel) {
|
fun set(newModels: List<Site>) {
|
||||||
models.add(model)
|
val formerModels = this.models
|
||||||
notifyItemInserted(models.size - 1)
|
this.models = newModels.toMutableList()
|
||||||
}
|
val diffResult = calculateDiff(SiteDiffCallback(formerModels, this.models))
|
||||||
|
diffResult.dispatchUpdatesTo(this)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(
|
||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
viewType: Int
|
viewType: Int
|
||||||
): ServerVH {
|
): SiteViewHolder {
|
||||||
val v = LayoutInflater.from(parent.context)
|
val v = LayoutInflater.from(parent.context)
|
||||||
.inflate(R.layout.list_item_server, parent, false)
|
.inflate(R.layout.list_item_server, parent, false)
|
||||||
return ServerVH(v, this)
|
return SiteViewHolder(v, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(
|
override fun onBindViewHolder(
|
||||||
holder: ServerVH,
|
holder: SiteViewHolder,
|
||||||
position: Int
|
position: Int
|
||||||
) {
|
) {
|
||||||
val model = models[position]
|
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)
|
* 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
|
package com.afollestad.nocknock.dialogs
|
||||||
|
|
||||||
|
@ -10,6 +20,7 @@ import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.afollestad.nocknock.BuildConfig
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
|
@ -24,8 +35,9 @@ class AboutDialog : DialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
return MaterialDialog(activity!!)
|
val context = activity ?: throw IllegalStateException("Oh no!")
|
||||||
.title(R.string.about)
|
return MaterialDialog(context)
|
||||||
|
.title(text = getString(R.string.app_name_x, BuildConfig.VERSION_NAME))
|
||||||
.positiveButton(R.string.dismiss)
|
.positiveButton(R.string.dismiss)
|
||||||
.message(R.string.about_body, html = true, lineHeightMultiplier = 1.4f)
|
.message(R.string.about_body, html = true, lineHeightMultiplier = 1.4f)
|
||||||
}
|
}
|
||||||
|
|
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)
|
* 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
|
package com.afollestad.nocknock.ui.addsite
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.Intent.ACTION_OPEN_DOCUMENT
|
||||||
|
import android.content.Intent.CATEGORY_OPENABLE
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.lifecycle.Observer
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.ValidationMode
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
import com.afollestad.nocknock.data.model.ValidationMode
|
||||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
||||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
import com.afollestad.nocknock.ui.viewsite.KEY_SITE
|
||||||
import com.afollestad.nocknock.data.indexToValidationMode
|
import com.afollestad.nocknock.utilities.ext.onTextChanged
|
||||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
|
||||||
import com.afollestad.nocknock.utilities.ext.injector
|
import com.afollestad.nocknock.utilities.livedata.distinct
|
||||||
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
|
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.conceal
|
import com.afollestad.nocknock.viewcomponents.ext.isVisibleCondition
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.onItemSelected
|
import com.afollestad.nocknock.viewcomponents.ext.onScroll
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.onLayout
|
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
|
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.trimmedText
|
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||||
|
import com.afollestad.vvalidator.form
|
||||||
|
import com.afollestad.vvalidator.form.Form
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout
|
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.doneBtn
|
import kotlinx.android.synthetic.main.activity_addsite.headersLayout
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.inputName
|
import kotlinx.android.synthetic.main.activity_addsite.inputName
|
||||||
|
import kotlinx.android.synthetic.main.activity_addsite.inputTags
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.inputUrl
|
import kotlinx.android.synthetic.main.activity_addsite.inputUrl
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.loadingProgress
|
import kotlinx.android.synthetic.main.activity_addsite.loadingProgress
|
||||||
|
import kotlinx.android.synthetic.main.activity_addsite.responseTimeoutInput
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.responseValidationMode
|
import kotlinx.android.synthetic.main.activity_addsite.responseValidationMode
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchTerm
|
import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchTerm
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.rootView
|
import kotlinx.android.synthetic.main.activity_addsite.retryPolicyLayout
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout
|
import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout
|
||||||
|
import kotlinx.android.synthetic.main.activity_addsite.scrollView
|
||||||
|
import kotlinx.android.synthetic.main.activity_addsite.sslCertificateBrowse
|
||||||
|
import kotlinx.android.synthetic.main.activity_addsite.sslCertificateInput
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning
|
import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.toolbar
|
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
|
import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
|
||||||
import javax.inject.Inject
|
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||||
import kotlin.coroutines.CoroutineContext
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import kotlin.math.max
|
import kotlinx.android.synthetic.main.include_app_bar.app_toolbar as appToolbar
|
||||||
import kotlin.properties.Delegates.notNull
|
import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
|
||||||
|
|
||||||
const val KEY_FAB_X = "fab_x"
|
|
||||||
const val KEY_FAB_Y = "fab_y"
|
|
||||||
const val KEY_FAB_SIZE = "fab_size"
|
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @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
|
private val viewModel by viewModel<AddSiteViewModel>()
|
||||||
var revealCx by notNull<Int>()
|
private lateinit var validationForm: Form
|
||||||
var revealCy by notNull<Int>()
|
|
||||||
var revealRadius by notNull<Float>()
|
|
||||||
|
|
||||||
@Inject lateinit var presenter: AddSitePresenter
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
injector().injectInto(this)
|
|
||||||
setContentView(R.layout.activity_addsite)
|
setContentView(R.layout.activity_addsite)
|
||||||
presenter.takeView(this)
|
setupUi()
|
||||||
|
setupValidation()
|
||||||
|
|
||||||
toolbar.setNavigationOnClickListener { closeActivityWithReveal() }
|
lifecycle.addObserver(viewModel)
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
// Populate view model with initial data
|
||||||
rootView.conceal()
|
val model = intent.getSerializableExtra(KEY_SITE) as? Site
|
||||||
rootView.onLayout {
|
model?.let { viewModel.prePopulateFromModel(model) }
|
||||||
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()
|
|
||||||
|
|
||||||
revealCx = fabX + fabSize / 2
|
// Loading
|
||||||
revealCy = (fabY + toolbar.measuredHeight + fabSize / 2)
|
loadingProgress.observe(this, viewModel.onIsLoading())
|
||||||
revealRadius = max(revealCx, revealCy).toFloat()
|
|
||||||
|
|
||||||
circularRevealActivity()
|
// Name
|
||||||
}
|
inputName.attachLiveData(this, viewModel.name)
|
||||||
}
|
|
||||||
|
|
||||||
inputUrl.setOnFocusChangeListener { _, hasFocus ->
|
// Tags
|
||||||
presenter.onUrlInputFocusChange(hasFocus, inputUrl.trimmedText())
|
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(
|
val validationOptionsAdapter = ArrayAdapter(
|
||||||
|
@ -91,98 +140,96 @@ class AddSiteActivity : AppCompatActivity(), AddSiteView {
|
||||||
resources.getStringArray(R.array.response_validation_options)
|
resources.getStringArray(R.array.response_validation_options)
|
||||||
)
|
)
|
||||||
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
||||||
|
|
||||||
responseValidationMode.adapter = validationOptionsAdapter
|
responseValidationMode.adapter = validationOptionsAdapter
|
||||||
responseValidationMode.onItemSelected(presenter::onValidationModeSelected)
|
|
||||||
|
|
||||||
doneBtn.setOnClickListener {
|
scrollView.onScroll {
|
||||||
val checkInterval = checkIntervalLayout.getSelectedCheckInterval()
|
appToolbar.elevation = if (it > appToolbar.measuredHeight / 2) {
|
||||||
val validationMode =
|
appToolbar.dimenFloat(R.dimen.default_elevation)
|
||||||
responseValidationMode.selectedItemPosition.indexToValidationMode()
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isClosing = true
|
// SSL certificate
|
||||||
presenter.commit(
|
sslCertificateBrowse.setOnClickListener {
|
||||||
name = inputName.trimmedText(),
|
val intent = Intent(ACTION_OPEN_DOCUMENT).apply {
|
||||||
url = inputUrl.trimmedText(),
|
addCategory(CATEGORY_OPENABLE)
|
||||||
checkInterval = checkInterval,
|
type = "*/*"
|
||||||
validationMode = validationMode,
|
}
|
||||||
validationContent = validationMode.validationContent()
|
startActivityForResult(intent, SELECT_CERT_FILE_RQ)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
private fun setupValidation() {
|
||||||
presenter.dropView()
|
validationForm = form {
|
||||||
super.onDestroy()
|
input(inputName, name = "Name") {
|
||||||
}
|
isNotEmpty().description(R.string.please_enter_name)
|
||||||
|
}
|
||||||
override fun setLoading() = loadingProgress.setLoading()
|
input(inputUrl, name = "URL") {
|
||||||
|
isNotEmpty().description(R.string.please_enter_url)
|
||||||
override fun setDoneLoading() = loadingProgress.setDone()
|
isUrl().description(R.string.please_enter_valid_url)
|
||||||
|
}
|
||||||
override fun showOrHideUrlSchemeWarning(show: Boolean) {
|
input(responseTimeoutInput, name = "Timeout", optional = true) {
|
||||||
textUrlWarning.showOrHide(show)
|
isNumber().greaterThan(0)
|
||||||
if (show) {
|
.description(R.string.please_enter_networkTimeout)
|
||||||
textUrlWarning.setText(R.string.warning_http_url)
|
}
|
||||||
}
|
input(responseValidationSearchTerm, name = "Search term") {
|
||||||
}
|
conditional(responseValidationSearchTerm.isVisibleCondition()) {
|
||||||
|
isNotEmpty().description(R.string.please_enter_search_term)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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!!)
|
// Check interval
|
||||||
} else {
|
checkIntervalLayout.attach(
|
||||||
null
|
valueData = viewModel.checkIntervalValue,
|
||||||
}
|
multiplierData = viewModel.checkIntervalUnit,
|
||||||
scriptInputLayout.setError(
|
form = validationForm
|
||||||
if (errors.javaScript != null) {
|
)
|
||||||
getString(errors.javaScript!!)
|
|
||||||
} else {
|
// Retry Policy
|
||||||
null
|
retryPolicyLayout.attach(
|
||||||
}
|
timesData = viewModel.retryPolicyTimes,
|
||||||
|
minutesData = viewModel.retryPolicyMinutes,
|
||||||
|
form = validationForm
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSiteAdded() {
|
override fun onResume() {
|
||||||
setResult(RESULT_OK)
|
super.onResume()
|
||||||
finish()
|
appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {
|
||||||
overridePendingTransition(R.anim.fade_out, R.anim.fade_out)
|
appToolbar.dimenFloat(R.dimen.default_elevation)
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun scopeWhileAttached(
|
override fun onActivityResult(
|
||||||
context: CoroutineContext,
|
requestCode: Int,
|
||||||
exec: ScopeReceiver
|
resultCode: Int,
|
||||||
) = rootView.scopeWhileAttached(context, exec)
|
resultData: Intent?
|
||||||
|
) {
|
||||||
override fun onBackPressed() = closeActivityWithReveal()
|
super.onActivityResult(requestCode, resultCode, resultData)
|
||||||
|
if (requestCode == SELECT_CERT_FILE_RQ && resultCode == RESULT_OK) {
|
||||||
private fun ValidationMode.validationContent() = when (this) {
|
sslCertificateInput.setText(resultData?.data?.toString() ?: "")
|
||||||
STATUS_CODE -> null
|
}
|
||||||
TERM_SEARCH -> responseValidationSearchTerm.trimmedText()
|
|
||||||
JAVASCRIPT -> scriptInputLayout.getCode()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
* 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
|
package com.afollestad.nocknock.ui.main
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.lifecycle.Observer
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
|
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.list.listItems
|
import com.afollestad.materialdialogs.list.listItems
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.adapter.ServerAdapter
|
import com.afollestad.nocknock.adapter.SiteAdapter
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
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.dialogs.AboutDialog
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
||||||
import com.afollestad.nocknock.utilities.ext.injector
|
import com.afollestad.nocknock.ui.NightMode.UNKNOWN
|
||||||
import com.afollestad.nocknock.utilities.ext.safeRegisterReceiver
|
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||||
import com.afollestad.nocknock.utilities.ext.safeUnregisterReceiver
|
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||||
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
|
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
|
|
||||||
import kotlinx.android.synthetic.main.activity_main.fab
|
import kotlinx.android.synthetic.main.activity_main.fab
|
||||||
import kotlinx.android.synthetic.main.activity_main.list
|
import kotlinx.android.synthetic.main.activity_main.list
|
||||||
import kotlinx.android.synthetic.main.activity_main.rootView
|
import kotlinx.android.synthetic.main.activity_main.loadingProgress
|
||||||
import kotlinx.android.synthetic.main.activity_main.toolbar
|
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||||
import kotlinx.android.synthetic.main.include_empty_view.emptyText
|
import kotlinx.android.synthetic.main.include_empty_view.emptyText
|
||||||
import javax.inject.Inject
|
import org.koin.android.ext.android.inject
|
||||||
import kotlin.coroutines.CoroutineContext
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
import kotlinx.android.synthetic.main.activity_main.tags_list as tagsList
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class MainActivity : AppCompatActivity(), MainView {
|
class MainActivity : DarkModeSwitchActivity() {
|
||||||
|
|
||||||
private val intentReceiver = object : BroadcastReceiver() {
|
private val notificationManager by inject<NockNotificationManager>()
|
||||||
override fun onReceive(
|
private val intentProvider by inject<IntentProvider>()
|
||||||
context: Context,
|
|
||||||
intent: Intent
|
internal val viewModel by viewModel<MainViewModel>()
|
||||||
) = presenter.onBroadcast(intent)
|
|
||||||
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
injector().injectInto(this)
|
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
presenter.takeView(this)
|
setupUi()
|
||||||
|
|
||||||
toolbar.inflateMenu(R.menu.menu_main)
|
notificationManager.createChannels()
|
||||||
toolbar.setOnMenuItemClickListener { item ->
|
|
||||||
if (item.itemId == R.id.about) {
|
lifecycle.run {
|
||||||
AboutDialog.show(this)
|
addObserver(viewModel)
|
||||||
}
|
addObserver(statusUpdateReceiver)
|
||||||
return@setOnMenuItemClickListener true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter = ServerAdapter(this::onSiteSelected)
|
viewModel.onSites()
|
||||||
|
.observe(this, Observer { siteAdapter.set(it) })
|
||||||
list.layoutManager = LinearLayoutManager(this)
|
viewModel.onEmptyTextVisibility()
|
||||||
list.adapter = adapter
|
.toViewVisibility(this, emptyText)
|
||||||
list.addItemDecoration(DividerItemDecoration(this, VERTICAL))
|
viewModel.onTags()
|
||||||
|
.observe(this, Observer { tagAdapter.set(it) })
|
||||||
fab.setOnClickListener { addSite() }
|
viewModel.onTagsListVisibility()
|
||||||
|
.toViewVisibility(this, tagsList)
|
||||||
|
loadingProgress.observe(this, viewModel.onIsLoading())
|
||||||
|
|
||||||
processIntent(intent)
|
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?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
intent?.let(::processIntent)
|
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(
|
private fun onSiteSelected(
|
||||||
model: ServerModel,
|
model: Site,
|
||||||
longClick: Boolean
|
longClick: Boolean
|
||||||
) {
|
) {
|
||||||
if (longClick) {
|
if (longClick) {
|
||||||
|
@ -131,8 +136,9 @@ class MainActivity : AppCompatActivity(), MainView {
|
||||||
title(R.string.options)
|
title(R.string.options)
|
||||||
listItems(R.array.site_long_options) { _, i, _ ->
|
listItems(R.array.site_long_options) { _, i, _ ->
|
||||||
when (i) {
|
when (i) {
|
||||||
0 -> presenter.refreshSite(model)
|
0 -> viewModel.refreshSite(model)
|
||||||
1 -> maybeRemoveSite(model)
|
1 -> addSiteForDuplication(model)
|
||||||
|
2 -> maybeRemoveSite(model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,63 +1,73 @@
|
||||||
/*
|
/**
|
||||||
* Licensed under Apache-2.0
|
|
||||||
*
|
|
||||||
* Designed and developed by Aidan Follestad (@afollestad)
|
* 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
|
package com.afollestad.nocknock.ui.main
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.nocknock.R
|
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.toHtml
|
||||||
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
||||||
import com.afollestad.nocknock.ui.addsite.KEY_FAB_SIZE
|
import com.afollestad.nocknock.ui.viewsite.KEY_SITE
|
||||||
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.ViewSiteActivity
|
import com.afollestad.nocknock.ui.viewsite.ViewSiteActivity
|
||||||
import com.afollestad.nocknock.utilities.providers.RealIntentProvider.Companion.KEY_VIEW_NOTIFICATION_MODEL
|
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 VIEW_SITE_RQ = 6923
|
||||||
internal const val ADD_SITE_RQ = 6969
|
internal const val ADD_SITE_RQ = 6969
|
||||||
|
|
||||||
|
// ADD
|
||||||
|
|
||||||
internal fun MainActivity.addSite() {
|
internal fun MainActivity.addSite() {
|
||||||
startActivityForResult(intentToAdd(fab.x, fab.y, fab.measuredWidth), ADD_SITE_RQ)
|
startActivityForResult(intentToAdd(), ADD_SITE_RQ)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MainActivity.intentToAdd(
|
internal fun MainActivity.addSiteForDuplication(site: Site) {
|
||||||
x: Float,
|
startActivityForResult(intentToAdd(site), ADD_SITE_RQ)
|
||||||
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.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)
|
startActivityForResult(intentToView(model), VIEW_SITE_RQ)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MainActivity.intentToView(model: ServerModel) =
|
private fun MainActivity.intentToView(model: Site) =
|
||||||
Intent(this, ViewSiteActivity::class.java).apply {
|
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 {
|
MaterialDialog(this).show {
|
||||||
title(R.string.remove_site)
|
title(R.string.remove_site)
|
||||||
message(text = context.getString(R.string.remove_site_prompt, model.name).toHtml())
|
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)
|
negativeButton(android.R.string.cancel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun MainActivity.processIntent(intent: Intent) {
|
internal fun MainActivity.processIntent(intent: Intent) {
|
||||||
if (intent.hasExtra(KEY_VIEW_NOTIFICATION_MODEL)) {
|
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)
|
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)
|
* 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
|
package com.afollestad.nocknock.ui.viewsite
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
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.os.Bundle
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.lifecycle.Observer
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.LAST_CHECK_NONE
|
import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.ValidationMode
|
import com.afollestad.nocknock.data.model.ValidationMode
|
||||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
||||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
import com.afollestad.nocknock.utilities.ext.onTextChanged
|
||||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
|
||||||
import com.afollestad.nocknock.data.indexToValidationMode
|
import com.afollestad.nocknock.utilities.livedata.distinct
|
||||||
import com.afollestad.nocknock.data.textRes
|
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||||
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.viewcomponents.ext.dimenFloat
|
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.onScroll
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
|
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.trimmedText
|
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.checkIntervalLayout
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.disableChecksButton
|
import kotlinx.android.synthetic.main.activity_viewsite.headersLayout
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.doneBtn
|
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.iconStatus
|
import kotlinx.android.synthetic.main.activity_viewsite.iconStatus
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.inputName
|
import kotlinx.android.synthetic.main.activity_viewsite.inputName
|
||||||
|
import kotlinx.android.synthetic.main.activity_viewsite.inputTags
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.inputUrl
|
import kotlinx.android.synthetic.main.activity_viewsite.inputUrl
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.loadingProgress
|
import kotlinx.android.synthetic.main.activity_viewsite.loadingProgress
|
||||||
|
import kotlinx.android.synthetic.main.activity_viewsite.responseTimeoutInput
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.responseValidationMode
|
import kotlinx.android.synthetic.main.activity_viewsite.responseValidationMode
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.responseValidationSearchTerm
|
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.scriptInputLayout
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.scrollView
|
import kotlinx.android.synthetic.main.activity_viewsite.scrollView
|
||||||
|
import kotlinx.android.synthetic.main.activity_viewsite.sslCertificateBrowse
|
||||||
|
import kotlinx.android.synthetic.main.activity_viewsite.sslCertificateInput
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.textLastCheckResult
|
import kotlinx.android.synthetic.main.activity_viewsite.textLastCheckResult
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck
|
import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
|
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.toolbar
|
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription
|
import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription
|
||||||
import javax.inject.Inject
|
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||||
import kotlin.coroutines.CoroutineContext
|
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) */
|
/** @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() {
|
private val intentProvider by inject<IntentProvider>()
|
||||||
override fun onReceive(
|
private val statusUpdateReceiver by lazy {
|
||||||
context: Context,
|
StatusUpdateIntentReceiver(application, intentProvider) {
|
||||||
intent: Intent
|
viewModel.setModel(it)
|
||||||
) = presenter.onBroadcast(intent)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
injector().injectInto(this)
|
|
||||||
setContentView(R.layout.activity_viewsite)
|
setContentView(R.layout.activity_viewsite)
|
||||||
|
|
||||||
|
// Populate view model with initial data
|
||||||
|
val model = intent.getSerializableExtra(KEY_SITE) as Site
|
||||||
|
viewModel.setModel(model)
|
||||||
|
|
||||||
|
setupUi()
|
||||||
|
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 {
|
toolbar.run {
|
||||||
|
setNavigationIcon(R.drawable.ic_action_close)
|
||||||
setNavigationOnClickListener { finish() }
|
setNavigationOnClickListener { finish() }
|
||||||
inflateMenu(R.menu.menu_viewsite)
|
inflateMenu(R.menu.menu_viewsite)
|
||||||
|
|
||||||
menu.findItem(R.id.refresh)
|
menu.findItem(R.id.refresh)
|
||||||
.setActionView(R.layout.menu_item_refresh_icon)
|
.setActionView(R.layout.menu_item_refresh_icon)
|
||||||
.apply {
|
.apply {
|
||||||
actionView.setOnClickListener { presenter.checkNow() }
|
actionView.setOnClickListener { viewModel.checkNow() }
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnMenuItemClickListener {
|
setOnMenuItemClickListener {
|
||||||
maybeRemoveSite()
|
when (it.itemId) {
|
||||||
return@setOnMenuItemClickListener true
|
R.id.remove -> maybeRemoveSite()
|
||||||
|
R.id.disableChecks -> maybeDisableChecks()
|
||||||
|
}
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollView.onScroll {
|
scrollView.onScroll {
|
||||||
toolbar.elevation = if (it > toolbar.height / 4) {
|
appToolbar.elevation = if (it > appToolbar.measuredHeight / 2) {
|
||||||
toolbar.dimenFloat(R.dimen.default_elevation)
|
appToolbar.dimenFloat(R.dimen.default_elevation)
|
||||||
} else {
|
} else {
|
||||||
0f
|
0f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inputUrl.setOnFocusChangeListener { _, hasFocus ->
|
|
||||||
presenter.onUrlInputFocusChange(hasFocus, inputUrl.trimmedText())
|
|
||||||
}
|
|
||||||
|
|
||||||
val validationOptionsAdapter = ArrayAdapter(
|
val validationOptionsAdapter = ArrayAdapter(
|
||||||
this,
|
this,
|
||||||
R.layout.list_item_spinner,
|
R.layout.list_item_spinner,
|
||||||
|
@ -107,150 +187,105 @@ class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
|
||||||
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
||||||
responseValidationMode.adapter = validationOptionsAdapter
|
responseValidationMode.adapter = validationOptionsAdapter
|
||||||
|
|
||||||
responseValidationMode.onItemSelected(presenter::onValidationModeSelected)
|
// Disabled button
|
||||||
|
viewModel.onDisableChecksVisibility()
|
||||||
|
.observe(this, Observer {
|
||||||
|
toolbar.menu.findItem(R.id.disableChecks)
|
||||||
|
.isVisible = it
|
||||||
|
})
|
||||||
|
|
||||||
doneBtn.setOnClickListener {
|
// Done item text
|
||||||
val checkInterval = checkIntervalLayout.getSelectedCheckInterval()
|
viewModel.onDoneButtonText()
|
||||||
val validationMode =
|
.observe(this, Observer {
|
||||||
responseValidationMode.selectedItemPosition.indexToValidationMode()
|
toolbar.menu.findItem(R.id.commit)
|
||||||
|
.setTitle(it)
|
||||||
|
})
|
||||||
|
|
||||||
presenter.commit(
|
// SSL certificate
|
||||||
name = inputName.trimmedText(),
|
sslCertificateBrowse.setOnClickListener {
|
||||||
url = inputUrl.trimmedText(),
|
val intent = Intent(ACTION_OPEN_DOCUMENT).apply {
|
||||||
checkInterval = checkInterval,
|
addCategory(CATEGORY_OPENABLE)
|
||||||
validationMode = validationMode,
|
type = "*/*"
|
||||||
validationContent = validationMode.validationContent()
|
}
|
||||||
)
|
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?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
presenter.onNewIntent(intent)
|
if (intent != null && intent.hasExtra(KEY_SITE)) {
|
||||||
}
|
val newModel = intent.getSerializableExtra(KEY_SITE) as Site
|
||||||
|
viewModel.setModel(newModel)
|
||||||
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 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)
|
* 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
|
package com.afollestad.nocknock.ui.viewsite
|
||||||
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.model.Status
|
||||||
import com.afollestad.nocknock.data.isPending
|
import com.afollestad.nocknock.data.model.isPending
|
||||||
import com.afollestad.nocknock.toHtml
|
import com.afollestad.nocknock.toHtml
|
||||||
import com.afollestad.nocknock.utilities.ext.animateRotation
|
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() {
|
internal fun ViewSiteActivity.maybeRemoveSite() {
|
||||||
val model = presenter.currentModel()
|
val model = viewModel.site
|
||||||
MaterialDialog(this).show {
|
MaterialDialog(this).show {
|
||||||
title(R.string.remove_site)
|
title(R.string.remove_site)
|
||||||
message(text = context.getString(R.string.remove_site_prompt, model.name).toHtml())
|
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)
|
negativeButton(android.R.string.cancel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun ViewSiteActivity.maybeDisableChecks() {
|
internal fun ViewSiteActivity.maybeDisableChecks() {
|
||||||
val model = presenter.currentModel()
|
val model = viewModel.site
|
||||||
MaterialDialog(this).show {
|
MaterialDialog(this).show {
|
||||||
title(R.string.disable_automatic_checks)
|
title(R.string.disable_automatic_checks)
|
||||||
message(
|
message(
|
||||||
text = context.getString(R.string.disable_automatic_checks_prompt, model.name).toHtml()
|
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)
|
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)
|
val refreshIcon = toolbar.menu.findItem(R.id.refresh)
|
||||||
.actionView as ImageView
|
.actionView as ImageView
|
||||||
|
if (status.isPending()) {
|
||||||
if (model.status.isPending()) {
|
|
||||||
refreshIcon.animateRotation()
|
refreshIcon.animateRotation()
|
||||||
} else {
|
} else {
|
||||||
refreshIcon.run {
|
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">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<size android:height="1dp"/>
|
<size android:height="1dp"/>
|
||||||
<solid android:color="@color/dividerColor"/>
|
<solid android:color="?dividerColor"/>
|
||||||
</shape>
|
</shape>
|
||||||
|
|
|
@ -4,6 +4,6 @@
|
||||||
android:viewportHeight="24.0"
|
android:viewportHeight="24.0"
|
||||||
android:viewportWidth="24.0">
|
android:viewportWidth="24.0">
|
||||||
<path
|
<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"/>
|
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>
|
</vector>
|
||||||
|
|
|
@ -4,6 +4,6 @@
|
||||||
android:viewportHeight="24.0"
|
android:viewportHeight="24.0"
|
||||||
android:viewportWidth="24.0">
|
android:viewportWidth="24.0">
|
||||||
<path
|
<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"/>
|
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>
|
</vector>
|
||||||
|
|
|
@ -4,6 +4,6 @@
|
||||||
android:viewportHeight="24.0"
|
android:viewportHeight="24.0"
|
||||||
android:viewportWidth="24.0">
|
android:viewportWidth="24.0">
|
||||||
<path
|
<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"/>
|
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>
|
</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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
@ -11,21 +10,13 @@
|
||||||
android:id="@+id/rootView"
|
android:id="@+id/rootView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?colorPrimary"
|
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
>
|
>
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
<include layout="@layout/include_app_bar"/>
|
||||||
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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
android:id="@+id/scrollView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
>
|
>
|
||||||
|
@ -34,66 +25,66 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingBottom="@dimen/content_inset"
|
android:paddingBottom="@dimen/content_inset_double"
|
||||||
android:paddingLeft="@dimen/content_inset"
|
android:paddingLeft="@dimen/content_inset"
|
||||||
android:paddingRight="@dimen/content_inset"
|
android:paddingRight="@dimen/content_inset"
|
||||||
|
android:paddingTop="@dimen/content_inset_half"
|
||||||
>
|
>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<TextView
|
||||||
android:id="@+id/nameTiLayout"
|
android:layout_marginTop="0dp"
|
||||||
android:layout_width="match_parent"
|
android:text="@string/site_name"
|
||||||
android:layout_height="wrap_content"
|
style="@style/InputForm.Header"
|
||||||
android:layout_marginLeft="-4dp"
|
/>
|
||||||
android:layout_marginRight="-4dp"
|
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
|
||||||
>
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<EditText
|
||||||
android:id="@+id/inputName"
|
android:id="@+id/inputName"
|
||||||
android:layout_width="match_parent"
|
android:hint="@string/site_name_hint"
|
||||||
android:layout_height="wrap_content"
|
android:inputType="textPersonName|textCapWords|textAutoCorrect"
|
||||||
android:hint="@string/site_name"
|
android:nextFocusDown="@+id/inputUrl"
|
||||||
android:inputType="textPersonName|textCapWords|textAutoCorrect"
|
tools:ignore="Autofill"
|
||||||
android:textColor="#FFFFFF"
|
style="@style/InputForm.Field"
|
||||||
style="@style/NockText.Body"
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
<TextView
|
||||||
|
android:text="@string/site_url"
|
||||||
|
style="@style/InputForm.Header"
|
||||||
|
/>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<EditText
|
||||||
android:id="@+id/urlTiLayout"
|
android:id="@+id/inputUrl"
|
||||||
android:layout_width="match_parent"
|
android:hint="@string/site_url_hint"
|
||||||
android:layout_height="wrap_content"
|
android:inputType="textUri"
|
||||||
android:layout_marginLeft="-4dp"
|
android:nextFocusDown="@+id/inputTags"
|
||||||
android:layout_marginRight="-4dp"
|
tools:ignore="Autofill"
|
||||||
android:layout_marginTop="@dimen/content_inset_half"
|
style="@style/InputForm.Field"
|
||||||
>
|
/>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/inputUrl"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/site_url"
|
|
||||||
android:inputType="textUri"
|
|
||||||
android:textColor="#FFFFFF"
|
|
||||||
style="@style/NockText.Body"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textUrlWarning"
|
android:id="@+id/textUrlWarning"
|
||||||
android:layout_width="wrap_content"
|
android:text="@string/warning_http_url"
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/list_text_spacing"
|
|
||||||
android:visibility="gone"
|
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/InputForm.FieldNote"
|
||||||
style="@style/NockText.Footnote"
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:text="@string/site_tags"
|
||||||
|
style="@style/InputForm.Header"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/inputTags"
|
||||||
|
android:digits=",.?-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ "
|
||||||
|
android:hint="@string/site_tags_hint"
|
||||||
|
android:inputType="text|textCapWords"
|
||||||
|
android:nextFocusDown="@+id/inputUrl"
|
||||||
|
tools:ignore="Autofill"
|
||||||
|
style="@style/InputForm.Field"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<include layout="@layout/include_divider"/>
|
<include layout="@layout/include_divider"/>
|
||||||
|
|
||||||
<com.afollestad.nocknock.viewcomponents.CheckIntervalLayout
|
<com.afollestad.nocknock.viewcomponents.interval.ValidationIntervalLayout
|
||||||
android:id="@+id/checkIntervalLayout"
|
android:id="@+id/checkIntervalLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -102,11 +93,8 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/responseValidationLabel"
|
android:id="@+id/responseValidationLabel"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
|
||||||
android:text="@string/response_validation_mode"
|
android:text="@string/response_validation_mode"
|
||||||
style="@style/NockText.SectionHeader"
|
style="@style/InputForm.Header"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Spinner
|
<Spinner
|
||||||
|
@ -126,16 +114,16 @@
|
||||||
android:hint="@string/search_term"
|
android:hint="@string/search_term"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:ignore="Autofill,TextFields"
|
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:id="@+id/scriptInputLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="@dimen/content_inset"
|
android:layout_marginBottom="@dimen/content_inset"
|
||||||
android:layout_marginTop="@dimen/content_inset_half"
|
android:layout_marginTop="@dimen/content_inset_half"
|
||||||
android:background="@color/colorPrimaryDark"
|
android:background="?scriptLayoutBackground"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -148,13 +136,75 @@
|
||||||
style="@style/NockText.Body.Light"
|
style="@style/NockText.Body.Light"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<include layout="@layout/include_divider"/>
|
||||||
android:id="@+id/doneBtn"
|
|
||||||
|
<com.afollestad.nocknock.viewcomponents.retrypolicy.RetryPolicyLayout
|
||||||
|
android:id="@+id/retryPolicyLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/content_inset_double"
|
android:layout_marginTop="@dimen/content_inset_more"
|
||||||
android:text="@string/add_site"
|
/>
|
||||||
style="@style/AccentButton"
|
|
||||||
|
<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>
|
</LinearLayout>
|
||||||
|
|
|
@ -15,12 +15,19 @@
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
>
|
>
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
<include layout="@layout/include_app_bar"/>
|
||||||
android:id="@+id/toolbar"
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/tags_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:theme="@style/MainToolbarTheme"
|
android:clipToPadding="false"
|
||||||
style="@style/MainToolbarStyle"
|
android:paddingBottom="@dimen/content_inset_half"
|
||||||
|
android:paddingEnd="@dimen/content_inset"
|
||||||
|
android:paddingStart="@dimen/content_inset"
|
||||||
|
android:paddingTop="@dimen/content_inset_half"
|
||||||
|
android:scrollbars="none"
|
||||||
|
android:visibility="gone"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -34,18 +41,31 @@
|
||||||
|
|
||||||
<include layout="@layout/include_empty_view"/>
|
<include layout="@layout/include_empty_view"/>
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/fab"
|
android:id="@+id/fab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="end|bottom"
|
android:layout_gravity="end|bottom"
|
||||||
android:layout_margin="@dimen/content_inset"
|
android:layout_marginBottom="@dimen/content_inset"
|
||||||
android:src="@drawable/ic_add"
|
android:layout_marginEnd="@dimen/content_inset_more"
|
||||||
app:backgroundTint="?colorAccent"
|
android:minHeight="64dp"
|
||||||
app:elevation="@dimen/fab_elevation"
|
android:paddingBottom="@dimen/content_inset_half"
|
||||||
app:fabSize="normal"
|
android:paddingEnd="@dimen/content_inset"
|
||||||
app:pressedTranslationZ="@dimen/fab_elevation_pressed"
|
android:paddingStart="@dimen/content_inset"
|
||||||
app:rippleColor="#40ffffff"
|
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>
|
</FrameLayout>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
@ -15,17 +14,7 @@
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- Background is applied again here so programmatic elevation works -->
|
<include layout="@layout/include_app_bar"/>
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
android:id="@+id/scrollView"
|
android:id="@+id/scrollView"
|
||||||
|
@ -37,15 +26,30 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingBottom="@dimen/content_inset"
|
android:paddingBottom="@dimen/content_inset_double"
|
||||||
android:paddingLeft="@dimen/content_inset"
|
android:paddingLeft="@dimen/content_inset"
|
||||||
android:paddingRight="@dimen/content_inset"
|
android:paddingRight="@dimen/content_inset"
|
||||||
android:paddingTop="@dimen/content_inset_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
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/content_inset_quarter"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
@ -66,27 +70,14 @@
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
>
|
>
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/inputName"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/site_name"
|
|
||||||
android:inputType="textPersonName|textCapWords|textAutoCorrect"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textColor="#FFFFFF"
|
|
||||||
android:transitionName="site_name"
|
|
||||||
tools:ignore="Autofill,UnusedAttribute"
|
|
||||||
style="@style/NockText.Body"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/inputUrl"
|
android:id="@+id/inputUrl"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/site_url"
|
android:hint="@string/site_url"
|
||||||
android:inputType="textUri"
|
android:inputType="textUri"
|
||||||
|
android:nextFocusDown="@+id/inputTags"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textColor="#FFFFFF"
|
|
||||||
android:transitionName="site_url"
|
android:transitionName="site_url"
|
||||||
tools:ignore="Autofill,UnusedAttribute"
|
tools:ignore="Autofill,UnusedAttribute"
|
||||||
style="@style/NockText.Body"
|
style="@style/NockText.Body"
|
||||||
|
@ -104,6 +95,19 @@
|
||||||
style="@style/NockText.Footnote"
|
style="@style/NockText.Footnote"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/inputTags"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:digits=",.?-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ "
|
||||||
|
android:hint="@string/site_tags_hint"
|
||||||
|
android:imeOptions="actionNext"
|
||||||
|
android:inputType="text|textCapWords"
|
||||||
|
android:singleLine="true"
|
||||||
|
tools:ignore="Autofill,UnusedAttribute"
|
||||||
|
style="@style/NockText.Body"
|
||||||
|
/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -115,20 +119,13 @@
|
||||||
android:layout_marginTop="@dimen/content_inset_less"
|
android:layout_marginTop="@dimen/content_inset_less"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<com.afollestad.nocknock.viewcomponents.CheckIntervalLayout
|
<com.afollestad.nocknock.viewcomponents.interval.ValidationIntervalLayout
|
||||||
android:id="@+id/checkIntervalLayout"
|
android:id="@+id/checkIntervalLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
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
|
<TextView
|
||||||
android:id="@+id/responseValidationLabel"
|
android:id="@+id/responseValidationLabel"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -153,19 +150,18 @@
|
||||||
android:layout_marginRight="-4dp"
|
android:layout_marginRight="-4dp"
|
||||||
android:layout_marginTop="-4dp"
|
android:layout_marginTop="-4dp"
|
||||||
android:hint="@string/search_term"
|
android:hint="@string/search_term"
|
||||||
android:textColor="#FFFFFF"
|
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:ignore="Autofill,TextFields"
|
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:id="@+id/scriptInputLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="@dimen/content_inset"
|
android:layout_marginBottom="@dimen/content_inset"
|
||||||
android:layout_marginTop="@dimen/content_inset_half"
|
android:layout_marginTop="@dimen/content_inset_half"
|
||||||
android:background="@color/colorPrimaryDark"
|
android:background="?scriptLayoutBackground"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -178,6 +174,90 @@
|
||||||
style="@style/NockText.Body.Light"
|
style="@style/NockText.Body.Light"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginTop="@dimen/content_inset_less"
|
||||||
|
android:background="?dividerColor"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<com.afollestad.nocknock.viewcomponents.retrypolicy.RetryPolicyLayout
|
||||||
|
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"/>
|
<include layout="@layout/include_divider"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -214,24 +294,6 @@
|
||||||
style="@style/NockText.Body"
|
style="@style/NockText.Body"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/doneBtn"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/content_inset_double"
|
|
||||||
android:text="@string/save_changes"
|
|
||||||
style="@style/AccentButton"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/disableChecksButton"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/content_inset_half"
|
|
||||||
android:text="@string/disable_automatic_checks"
|
|
||||||
style="@style/PrimaryDarkButton"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</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_width="match_parent"
|
||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
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
|
<item
|
||||||
android:id="@+id/about"
|
android:id="@+id/about"
|
||||||
android:title="@string/about"/>
|
android:title="@string/about"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/dark_mode"
|
||||||
|
android:checkable="true"
|
||||||
|
android:title="@string/dark_mode"/>
|
||||||
</menu>
|
</menu>
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/commit"
|
||||||
|
android:icon="@drawable/ic_check"
|
||||||
|
android:title="@string/save_changes"
|
||||||
|
app:showAsAction="ifRoom"/>
|
||||||
<item
|
<item
|
||||||
android:id="@+id/refresh"
|
android:id="@+id/refresh"
|
||||||
android:icon="@drawable/ic_action_refresh"
|
android:icon="@drawable/ic_action_refresh"
|
||||||
android:title="@string/refresh_status"
|
android:title="@string/refresh_status"
|
||||||
app:showAsAction="ifRoom"/>
|
app:showAsAction="ifRoom"/>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/remove"
|
android:id="@+id/remove"
|
||||||
android:icon="@drawable/ic_action_delete"
|
android:icon="@drawable/ic_action_delete"
|
||||||
android:title="@string/remove_site"
|
android:title="@string/remove_site"
|
||||||
app:showAsAction="ifRoom"/>
|
app:showAsAction="ifRoom"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/disableChecks"
|
||||||
|
android:title="@string/disable_automatic_checks"
|
||||||
|
/>
|
||||||
</menu>
|
</menu>
|
||||||
|
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 5.4 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 7.7 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 15 KiB |
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">
|
<string-array name="site_long_options" translatable="false">
|
||||||
<item>@string/refresh_status</item>
|
<item>@string/refresh_status</item>
|
||||||
|
<item>@string/duplicate_and_modify</item>
|
||||||
<item>@string/remove_site</item>
|
<item>@string/remove_site</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<color name="colorPrimary">#455A64</color>
|
<color name="colorPrimary_lightTheme">#FFFFFF</color>
|
||||||
<color name="colorPrimaryDark">#37474F</color>
|
<color name="colorPrimaryDark_lightTheme">#F5F5F5</color>
|
||||||
<color name="colorAccent">#FF6E40</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>
|
</resources>
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<dimen name="empty_text_size">28sp</dimen>
|
<dimen name="empty_text_size">28sp</dimen>
|
||||||
|
|
||||||
<dimen name="list_text_spacing">6dp</dimen>
|
<dimen name="list_text_spacing">6dp</dimen>
|
||||||
|
<dimen name="toolbar_elevation">4dp</dimen>
|
||||||
<dimen name="fab_elevation">4dp</dimen>
|
|
||||||
<dimen name="fab_elevation_pressed">8dp</dimen>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#758F9A</color>
|
||||||
|
</resources>
|
|
@ -1,74 +1,90 @@
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<string name="app_name">Nock Nock</string>
|
<string name="app_name">Nock Nock</string>
|
||||||
|
<string name="app_name_x">Nock Nock %1$s</string>
|
||||||
|
|
||||||
<string name="no_sites_added">No sites added!</string>
|
<string name="no_sites_added">No sites added!</string>
|
||||||
|
|
||||||
<string name="about">About</string>
|
<string name="about">About</string>
|
||||||
<string name="about_body"><![CDATA[
|
<string name="about_body"><![CDATA[
|
||||||
<b>Nock Nock</b>, a simple app designed by <b>Aidan Follestad</b>.<br/>
|
A simple app designed by <b>Aidan Follestad</b>.<br/>
|
||||||
<a href=\'https://aidanfollestad.com\'>Website</a>
|
<a href=\'https://af.codes\'>Website</a>
|
||||||
<a href=\'https://twitter.com/afollestad\'>Twitter</a>
|
<a href=\'https://twitter.com/afollestad\'>Twitter</a>
|
||||||
<a href=\'https://github.com/afollestad\'>GitHub</a>
|
<a href=\'https://github.com/afollestad\'>GitHub</a>
|
||||||
<a href=\'https://www.linkedin.com/in/afollestad\'>LinkedIn</a>
|
<a href=\'https://www.linkedin.com/in/afollestad\'>LinkedIn</a>
|
||||||
<br/><br/><i>Nock Nock is open source! Check out the <a href=\'https://github.com/afollestad/nock-nock\'>GitHub page</a>!</i>
|
<br/><br/><i>Nock Nock is open source! Check out the <a href=\'https://github.com/afollestad/nock-nock\'>GitHub page</a>!</i>
|
||||||
<br/>Icon by <a href=\'https://plus.google.com/+KevinAguilarC\'>Kevin Aguilar</a> of <b>221 Pixels</b>.
|
<br/>Icon by <a href=\'https://plus.google.com/+KevinAguilarC\'>Kevin Aguilar</a> of <b>221 Pixels</b>.
|
||||||
]]></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="dismiss">Dismiss</string>
|
||||||
<string name="add_site">Add Site</string>
|
<string name="add_site">Add Site</string>
|
||||||
<string name="site_name">Site Name</string>
|
<string name="site_name">Site Name</string>
|
||||||
|
<string name="site_name_hint">Site display name</string>
|
||||||
<string name="site_url">Site URL</string>
|
<string name="site_url">Site URL</string>
|
||||||
|
<string name="site_url_hint">https://yoursite.com</string>
|
||||||
|
<string name="site_tags">Site Tags</string>
|
||||||
|
<string name="site_tags_hint">e.g. One,Two,Three</string>
|
||||||
|
<string name="site_tags_hint_full">Tags (e.g. One,Two,Three)</string>
|
||||||
<string name="please_enter_name">Please enter a name!</string>
|
<string name="please_enter_name">Please enter a name!</string>
|
||||||
<string name="please_enter_url">Please enter a URL.</string>
|
<string name="please_enter_url">Please enter a URL.</string>
|
||||||
<string name="please_enter_valid_url">Please enter a valid URL.</string>
|
<string name="please_enter_valid_url">Please enter a valid URL.</string>
|
||||||
<string name="please_enter_check_interval">Please input a check interval.</string>
|
|
||||||
<string name="please_enter_search_term">Please input a search term.</string>
|
<string name="please_enter_search_term">Please input a search term.</string>
|
||||||
<string name="please_enter_javaScript">Please input a validation script.</string>
|
<string name="please_enter_networkTimeout">Please enter a network timeout greater than 0.</string>
|
||||||
|
<string name="please_enter_validCertUri">Certificate should be a valid file or content URI.</string>
|
||||||
|
|
||||||
<string name="options">Options</string>
|
<string name="options">Options</string>
|
||||||
<string name="already_checking_sites">Already checking sites!</string>
|
|
||||||
<string name="remove_site">Remove Site</string>
|
<string name="remove_site">Remove Site</string>
|
||||||
|
<string name="duplicate_and_modify">Duplicate and Modify</string>
|
||||||
<string name="remove_site_prompt"><![CDATA[Remove <b>%1$s</b> from your sites?]]></string>
|
<string name="remove_site_prompt"><![CDATA[Remove <b>%1$s</b> from your sites?]]></string>
|
||||||
<string name="remove">Remove</string>
|
<string name="remove">Remove</string>
|
||||||
<string name="save_changes">Save Changes</string>
|
<string name="save_changes">Save Changes</string>
|
||||||
<string name="view_site">View Site</string>
|
<string name="view_site">View Site</string>
|
||||||
<string name="last_check_result">Last Check Result</string>
|
<string name="last_check_result">Last Validation Result</string>
|
||||||
<string name="next_check">Next Check</string>
|
<string name="next_check">Next Validation</string>
|
||||||
<string name="next_check_x">Next Check: %1$s</string>
|
<string name="next_check_x">Next Validation: %1$s</string>
|
||||||
<string name="now">Now</string>
|
<string name="now">Now</string>
|
||||||
<string name="none_turned_off">None (turned off)</string>
|
|
||||||
<string name="none">None</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[
|
<string name="disable_automatic_checks_prompt"><![CDATA[
|
||||||
Disable automatic checks for <b>%1$s</b>? The site will not be validated in the background
|
Disable automatic validation for <b>%1$s</b>? The site will not be checked in the background
|
||||||
until you re-enable checks for it. You can still manually perform checks by tapping the
|
until you re-enable validation by tapping the checkmark (Save) icon. You can still manually
|
||||||
Refresh icon at the top of this page.
|
perform validation by tapping the Refresh icon at the top of this page.
|
||||||
]]></string>
|
]]></string>
|
||||||
<string name="disable">Disable</string>
|
<string name="disable">Disable</string>
|
||||||
<string name="renable_and_save_changes">Enable 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="refresh_status">Refresh Status</string>
|
||||||
|
|
||||||
<string name="warning_http_url">
|
<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.
|
use an HTTP URL.
|
||||||
</string>
|
</string>
|
||||||
<string name="response_validation_mode">Response Validation Mode</string>
|
<string name="response_validation_mode">Response Validation Mode</string>
|
||||||
<string name="search_term">Search term…</string>
|
<string name="search_term">Search term…</string>
|
||||||
|
|
||||||
<string name="validation_mode_status_desc">
|
<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>
|
||||||
<string name="validation_mode_term_desc">
|
<string name="validation_mode_term_desc">
|
||||||
The status code check is done first. If it\'s successful, the response body is checked.
|
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>
|
||||||
<string name="validation_mode_javascript_desc">
|
<string name="validation_mode_javascript_desc">
|
||||||
The status code check is done first. If it\'s successful, the response body is passed to the
|
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.
|
exception to pass custom error messages to Nock Nock.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
|
<string name="install_web_browser">Please install a web browser app, such as Google Chrome.</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1,73 +1,12 @@
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
<style name="AppTheme" parent="AppThemeParent"/>
|
||||||
<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>
|
<style name="AppTheme.Dark" parent="AppThemeParent.Dark"/>
|
||||||
|
|
||||||
<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="PrimaryDarkButton" parent="Widget.MaterialComponents.Button">
|
<style name="PrimaryDarkButton" parent="Widget.MaterialComponents.Button">
|
||||||
<item name="android:textColor">#fff</item>
|
<item name="android:textColor">#fff</item>
|
||||||
<item name="backgroundTint">@color/colorPrimaryDark</item>
|
<item name="backgroundTint">@color/darkerGray</item>
|
||||||
<item name="android:fontFamily">@font/lato</item>
|
<item name="android:fontFamily">@font/lato</item>
|
||||||
</style>
|
</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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|