mirror of
https://github.com/afollestad/nock-nock.git
synced 2025-04-20 11:36:10 +00:00
Compare commits
151 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 | ||
|
cf39207c08 | ||
|
08ddf1ca03 | ||
|
b750957d79 | ||
|
00973b9c71 | ||
|
c6307f5061 | ||
|
2d81575e4b | ||
|
cf802dfa2f | ||
|
225434b41a | ||
|
f87e1438d2 | ||
|
b36b41ca9d | ||
|
cfe9df9225 | ||
|
62ce516972 | ||
|
1a66d2bbd7 | ||
|
8193dd017d | ||
|
ef73245831 | ||
|
77a98b161b | ||
|
6dfff5bb12 | ||
|
c7096e8746 | ||
|
a8bcc60496 | ||
|
3507aec3db | ||
|
7e500fc1ed | ||
|
999502802a | ||
|
eeaa68dbe2 | ||
|
d1672a6c5e | ||
|
0785c36b2a | ||
|
c7a8148d3c | ||
|
56eb67d825 | ||
|
8a6cb18ae6 | ||
|
bc6a7bb559 | ||
|
c711ca9e57 | ||
|
dfb01f0304 | ||
|
8ec5280a01 | ||
|
ac36b94233 | ||
|
32b17ed5d3 | ||
|
beece8c0c1 | ||
|
680f6b2931 | ||
|
b32880ca9d |
263 changed files with 11374 additions and 2951 deletions
28
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
Normal file
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
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.
|
8
.github/pull_request_template.md
vendored
Normal file
8
.github/pull_request_template.md
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
### Guidelines
|
||||
|
||||
1. You must run the `spotlessApply` task before committing, either through Android Studio or with `./gradlew spotlessApply`.
|
||||
2. A PR should be focused and contained. If you are changing multiple unrelated things, they should be in separate PRs.
|
||||
3. A PR should fix a bug or solve a problem - something that only you would use is not necessarily something that should be published.
|
||||
4. Give your PR a detailed title and description - look over your code one last time before actually creating the PR. Give it a self-review.
|
||||
|
||||
**If you do not follow the guidelines, your PR will be rejected.**
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -180,4 +180,6 @@ gradle-app.setting
|
|||
.gradletasknamecache
|
||||
|
||||
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||
# gradle/wrapper/gradle-wrapper.properties
|
||||
# gradle/wrapper/gradle-wrapper.properties
|
||||
|
||||
app/google-services.json
|
1
.idea/.name
generated
1
.idea/.name
generated
|
@ -1 +0,0 @@
|
|||
nock-nock
|
22
.idea/compiler.xml
generated
22
.idea/compiler.xml
generated
|
@ -1,22 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<resourceExtensions />
|
||||
<wildcardResourcePatterns>
|
||||
<entry name="!?*.java" />
|
||||
<entry name="!?*.form" />
|
||||
<entry name="!?*.class" />
|
||||
<entry name="!?*.groovy" />
|
||||
<entry name="!?*.scala" />
|
||||
<entry name="!?*.flex" />
|
||||
<entry name="!?*.kt" />
|
||||
<entry name="!?*.clj" />
|
||||
<entry name="!?*.aj" />
|
||||
</wildcardResourcePatterns>
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="false">
|
||||
<processorPath useClasspath="true" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
</project>
|
3
.idea/copyright/profiles_settings.xml
generated
3
.idea/copyright/profiles_settings.xml
generated
|
@ -1,3 +0,0 @@
|
|||
<component name="CopyrightManager">
|
||||
<settings default="" />
|
||||
</component>
|
6
.idea/encodings.xml
generated
6
.idea/encodings.xml
generated
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="PROJECT" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
39
.idea/misc.xml
generated
39
.idea/misc.xml
generated
|
@ -1,46 +1,49 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EntryPointsManager">
|
||||
<entry_points version="2.0" />
|
||||
<component name="CMakeSettings">
|
||||
<configurations>
|
||||
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
|
||||
</configurations>
|
||||
</component>
|
||||
<component name="NullableNotNullManager">
|
||||
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
|
||||
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
|
||||
<option name="myNullables">
|
||||
<value>
|
||||
<list size="4">
|
||||
<list size="10">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
||||
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
|
||||
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
||||
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
|
||||
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
|
||||
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
|
||||
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
|
||||
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myNotNulls">
|
||||
<value>
|
||||
<list size="4">
|
||||
<list size="9">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
||||
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
|
||||
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
|
||||
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
|
||||
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
|
||||
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
|
||||
<OptionsSetting value="true" id="Add" />
|
||||
<OptionsSetting value="true" id="Remove" />
|
||||
<OptionsSetting value="true" id="Checkout" />
|
||||
<OptionsSetting value="true" id="Update" />
|
||||
<OptionsSetting value="true" id="Status" />
|
||||
<OptionsSetting value="true" id="Edit" />
|
||||
<ConfirmationsSetting value="0" id="Add" />
|
||||
<ConfirmationsSetting value="0" id="Remove" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
|
|
7
.idea/modules.xml
generated
7
.idea/modules.xml
generated
|
@ -3,7 +3,12 @@
|
|||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/common/common.iml" filepath="$PROJECT_DIR$/common/common.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/data/data.iml" filepath="$PROJECT_DIR$/data/data.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/engine/engine.iml" filepath="$PROJECT_DIR$/engine/engine.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/nock-nock.iml" filepath="$PROJECT_DIR$/nock-nock.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/notifications/notifications.iml" filepath="$PROJECT_DIR$/notifications/notifications.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/viewcomponents/viewcomponents.iml" filepath="$PROJECT_DIR$/viewcomponents/viewcomponents.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
|
|
22
.travis.yml
22
.travis.yml
|
@ -1,22 +0,0 @@
|
|||
language: android
|
||||
jdk: oraclejdk8
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-24.0.1
|
||||
- android-24
|
||||
- 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:
|
||||
- '.+'
|
Binary file not shown.
|
@ -1,9 +1,8 @@
|
|||
## Nock Nock
|
||||
|
||||
[](https://travis-ci.org/afollestad/nock-nock)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
|
||||

|
||||

|
||||
|
||||
Nock Nock is a simple app which allows you to monitor your websites for maximum uptime.
|
||||
|
||||
|
@ -11,4 +10,4 @@ The app will automatically knock on the door of your websites (or web servers) o
|
|||
to make sure they are up and responding successfully. If something is wrong, you get a notification telling you so.
|
||||
|
||||
<br/>
|
||||
<a href='https://play.google.com/store/apps/details?id=com.afollestad.nocknock&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png' width="200px"/></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=com.afollestad.nocknock&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="200px"/></a>
|
||||
|
|
104
app/build.gradle
104
app/build.gradle
|
@ -1,43 +1,79 @@
|
|||
apply from: '../dependencies.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
android {
|
||||
compileSdkVersion 24
|
||||
buildToolsVersion "24.0.1"
|
||||
compileSdkVersion versions.compileSdk
|
||||
buildToolsVersion versions.buildTools
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.afollestad.nocknock"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 24
|
||||
versionCode 13
|
||||
versionName "0.1.3.0"
|
||||
defaultConfig {
|
||||
applicationId "com.afollestad.nocknock"
|
||||
minSdkVersion versions.minSdk
|
||||
targetSdkVersion versions.compileSdk
|
||||
versionCode versions.publishVersionCode
|
||||
versionName versions.publishVersion
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
jackOptions {
|
||||
enabled true
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility 1.8
|
||||
targetCompatibility 1.8
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
exclude 'META-INF/atomicfu.kotlin_module'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile 'com.android.support:appcompat-v7:24.2.0'
|
||||
compile 'com.android.support:design:24.2.0'
|
||||
compile 'com.afollestad.material-dialogs:core:0.9.0.1'
|
||||
compile 'com.afollestad.material-dialogs:commons:0.9.0.1'
|
||||
compile 'com.afollestad:bridge:3.2.5'
|
||||
compile 'com.afollestad:inquiry:3.2.1'
|
||||
compile files('libs/rhino-1.7.7.1.jar')
|
||||
}
|
||||
implementation project(':common')
|
||||
implementation project(':engine')
|
||||
implementation project(':data')
|
||||
implementation project(':notifications')
|
||||
implementation project(':viewcomponents')
|
||||
|
||||
// Google/AppCompat
|
||||
implementation 'androidx.appcompat:appcompat:' + versions.androidxCore
|
||||
implementation 'androidx.recyclerview:recyclerview:' + versions.androidxRecyclerView
|
||||
implementation 'com.google.android.material:material:' + versions.googleMaterial
|
||||
implementation 'androidx.browser:browser:' + versions.androidxBrowser
|
||||
implementation 'com.google.firebase:firebase-core:' + versions.firebaseCore
|
||||
|
||||
// Lifecycle
|
||||
kapt 'androidx.lifecycle:lifecycle-compiler:' + versions.lifecycle
|
||||
|
||||
// Kotlin
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin
|
||||
|
||||
// JOIN
|
||||
implementation 'org.koin:koin-android:' + versions.koin
|
||||
implementation 'org.koin:koin-androidx-scope:' + versions.koin
|
||||
implementation 'org.koin:koin-androidx-viewmodel:' + versions.koin
|
||||
|
||||
// afollestad
|
||||
implementation 'com.afollestad.material-dialogs:core:' + versions.materialDialogs
|
||||
|
||||
// Debugging
|
||||
implementation 'com.jakewharton.timber:timber:' + versions.timber
|
||||
implementation("com.crashlytics.sdk.android:crashlytics:${versions.fabric}") {
|
||||
transitive = true
|
||||
}
|
||||
|
||||
// Testing
|
||||
testImplementation 'junit:junit:' + versions.junit
|
||||
testImplementation 'org.mockito:mockito-core:' + versions.mockito
|
||||
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:' + versions.mockitoKotlin
|
||||
testImplementation 'com.google.truth:truth:' + versions.truth
|
||||
testImplementation 'androidx.arch.core:core-testing:' + versions.archTesting
|
||||
|
||||
// UI testing
|
||||
androidTestImplementation 'androidx.test:runner:' + versions.androidxTestRunner
|
||||
androidTestImplementation 'androidx.test:rules:' + versions.androidxTestRunner
|
||||
}
|
||||
|
||||
apply from: '../spotless.gradle'
|
||||
apply from: '../mock/mock.gradle'
|
||||
|
||||
apply plugin: "io.fabric"
|
||||
apply plugin: 'com.google.gms.google-services'
|
Binary file not shown.
17
app/proguard-rules.pro
vendored
17
app/proguard-rules.pro
vendored
|
@ -1,17 +0,0 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in C:\Users\drumm\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
|
@ -3,60 +3,57 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.afollestad.nocknock">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
|
||||
<application
|
||||
android:name=".NockNockApp"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
|
||||
|
||||
<activity
|
||||
android:name="com.afollestad.nocknock.ui.MainActivity"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<activity
|
||||
android:name="com.afollestad.nocknock.ui.main.MainActivity"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.afollestad.nocknock.ui.AddSiteActivity"
|
||||
android:label="@string/add_site"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/AppTheme.Transparent"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
<activity
|
||||
android:name="com.afollestad.nocknock.ui.addsite.AddSiteActivity"
|
||||
android:label="@string/add_site"
|
||||
android:launchMode="singleTop"
|
||||
android:windowSoftInputMode="stateHidden"/>
|
||||
|
||||
<activity
|
||||
android:name="com.afollestad.nocknock.ui.ViewSiteActivity"
|
||||
android:label="@string/view_site"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/AppTheme.Ink"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
<activity
|
||||
android:name="com.afollestad.nocknock.ui.viewsite.ViewSiteActivity"
|
||||
android:label="@string/view_site"
|
||||
android:launchMode="singleTop"
|
||||
android:windowSoftInputMode="stateHidden"/>
|
||||
|
||||
<service
|
||||
android:name="com.afollestad.nocknock.services.CheckService"
|
||||
android:enabled="true"
|
||||
android:label="Site Check Service" />
|
||||
<service
|
||||
android:name=".engine.validation.ValidationJob"
|
||||
android:label="@string/check_service_name"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"/>
|
||||
|
||||
<receiver android:name=".receivers.BootReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".engine.validation.BootReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".receivers.ConnectivityReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
||||
<action android:name="android.net.wifi.WIFI_STATE_CHANGED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<meta-data
|
||||
android:name="preloaded_fonts"
|
||||
android:resource="@array/preloaded_fonts"/>
|
||||
|
||||
</application>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
92
app/src/main/java/com/afollestad/nocknock/AppExt.kt
Normal file
92
app/src/main/java/com/afollestad/nocknock/AppExt.kt
Normal file
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.app.Application.ActivityLifecycleCallbacks
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||
import androidx.core.text.HtmlCompat.fromHtml
|
||||
import com.afollestad.materialdialogs.utils.MDUtil.resolveColor
|
||||
import com.afollestad.nocknock.utilities.ext.toUri
|
||||
import com.afollestad.nocknock.utilities.ui.toast
|
||||
|
||||
typealias ActivityLifeChange = (activity: Activity, resumed: Boolean) -> Unit
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
fun Application.onActivityLifeChange(cb: ActivityLifeChange) {
|
||||
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
|
||||
override fun onActivitySaveInstanceState(
|
||||
activity: Activity?,
|
||||
outState: Bundle?
|
||||
) = Unit
|
||||
|
||||
override fun onActivityPaused(activity: Activity) = cb(activity, false)
|
||||
|
||||
override fun onActivityResumed(activity: Activity) = cb(activity, true)
|
||||
|
||||
override fun onActivityStarted(activity: Activity) = Unit
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) = Unit
|
||||
|
||||
override fun onActivityStopped(activity: Activity) = Unit
|
||||
|
||||
override fun onActivityCreated(
|
||||
activity: Activity?,
|
||||
savedInstanceState: Bundle?
|
||||
) = Unit
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
79
app/src/main/java/com/afollestad/nocknock/NockNockApp.kt
Normal file
79
app/src/main/java/com/afollestad/nocknock/NockNockApp.kt
Normal file
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@file:Suppress("unused")
|
||||
|
||||
package com.afollestad.nocknock
|
||||
|
||||
import android.app.Application
|
||||
import com.afollestad.nocknock.BuildConfig.DEBUG
|
||||
import com.afollestad.nocknock.engine.engineModule
|
||||
import com.afollestad.nocknock.koin.mainModule
|
||||
import com.afollestad.nocknock.koin.prefModule
|
||||
import com.afollestad.nocknock.koin.viewModelModule
|
||||
import com.afollestad.nocknock.logging.FabricTree
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.notifications.notificationsModule
|
||||
import com.afollestad.nocknock.utilities.commonModule
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import io.fabric.sdk.android.Fabric
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.ext.android.startKoin
|
||||
import timber.log.Timber
|
||||
import timber.log.Timber.DebugTree
|
||||
import timber.log.Timber.d as log
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class NockNockApp : Application() {
|
||||
|
||||
private var resumedActivities: Int = 0
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
if (DEBUG) {
|
||||
Timber.plant(DebugTree())
|
||||
}
|
||||
|
||||
Timber.plant(FabricTree())
|
||||
Fabric.with(this, Crashlytics())
|
||||
|
||||
val modules = listOf(
|
||||
prefModule,
|
||||
mainModule,
|
||||
engineModule,
|
||||
commonModule,
|
||||
notificationsModule,
|
||||
viewModelModule
|
||||
)
|
||||
startKoin(
|
||||
androidContext = this,
|
||||
modules = modules
|
||||
)
|
||||
|
||||
val nockNotificationManager by inject<NockNotificationManager>()
|
||||
onActivityLifeChange { activity, resumed ->
|
||||
if (resumed) {
|
||||
resumedActivities++
|
||||
log("Activity resumed: $activity, resumedActivities = $resumedActivities")
|
||||
} else {
|
||||
resumedActivities--
|
||||
log("Activity paused: $activity, resumedActivities = $resumedActivities")
|
||||
}
|
||||
check(resumedActivities >= 0) { "resumedActivities can't go below 0." }
|
||||
nockNotificationManager.setIsAppOpen(resumedActivities > 0)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,171 +0,0 @@
|
|||
package com.afollestad.nocknock.adapter;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.afollestad.nocknock.R;
|
||||
import com.afollestad.nocknock.api.ServerModel;
|
||||
import com.afollestad.nocknock.api.ServerStatus;
|
||||
import com.afollestad.nocknock.util.TimeUtil;
|
||||
import com.afollestad.nocknock.views.StatusImageView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
public class ServerAdapter extends RecyclerView.Adapter<ServerAdapter.ServerVH> {
|
||||
|
||||
private final Object LOCK = new Object();
|
||||
private ArrayList<ServerModel> mServers;
|
||||
private ClickListener mListener;
|
||||
|
||||
public interface ClickListener {
|
||||
void onSiteSelected(int index, ServerModel model, boolean longClick);
|
||||
}
|
||||
|
||||
public void performClick(int index, boolean longClick) {
|
||||
if (mListener != null) {
|
||||
mListener.onSiteSelected(index, mServers.get(index), longClick);
|
||||
}
|
||||
}
|
||||
|
||||
public ServerAdapter(ClickListener listener) {
|
||||
mListener = listener;
|
||||
mServers = new ArrayList<>(2);
|
||||
}
|
||||
|
||||
public void add(ServerModel model) {
|
||||
mServers.add(model);
|
||||
notifyItemInserted(mServers.size() - 1);
|
||||
}
|
||||
|
||||
public void update(int index, ServerModel model) {
|
||||
mServers.set(index, model);
|
||||
notifyItemChanged(index);
|
||||
}
|
||||
|
||||
public void update(ServerModel model) {
|
||||
synchronized (LOCK) {
|
||||
for (int i = 0; i < mServers.size(); i++) {
|
||||
if (mServers.get(i).id == model.id) {
|
||||
update(i, model);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(int index) {
|
||||
mServers.remove(index);
|
||||
notifyItemRemoved(index);
|
||||
}
|
||||
|
||||
public void remove(ServerModel model) {
|
||||
synchronized (LOCK) {
|
||||
for (int i = 0; i < mServers.size(); i++) {
|
||||
if (mServers.get(i).id == model.id) {
|
||||
remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void set(ServerModel[] models) {
|
||||
if (models == null || models.length == 0) {
|
||||
mServers.clear();
|
||||
return;
|
||||
}
|
||||
mServers = new ArrayList<>(models.length);
|
||||
Collections.addAll(mServers, models);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
mServers.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerAdapter.ServerVH onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
final View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_server, parent, false);
|
||||
return new ServerVH(v, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ServerAdapter.ServerVH holder, int position) {
|
||||
final ServerModel model = mServers.get(position);
|
||||
|
||||
holder.textName.setText(model.name);
|
||||
holder.textUrl.setText(model.url);
|
||||
holder.iconStatus.setStatus(model.status);
|
||||
|
||||
switch (model.status) {
|
||||
case ServerStatus.OK:
|
||||
holder.textStatus.setText(R.string.everything_checks_out);
|
||||
break;
|
||||
case ServerStatus.WAITING:
|
||||
holder.textStatus.setText(R.string.waiting);
|
||||
break;
|
||||
case ServerStatus.CHECKING:
|
||||
holder.textStatus.setText(R.string.checking_status);
|
||||
break;
|
||||
case ServerStatus.ERROR:
|
||||
holder.textStatus.setText(model.reason);
|
||||
break;
|
||||
}
|
||||
|
||||
if (model.checkInterval <= 0) {
|
||||
holder.textInterval.setText("");
|
||||
} else {
|
||||
final long now = System.currentTimeMillis();
|
||||
final long nextCheck = model.lastCheck + model.checkInterval;
|
||||
final long difference = nextCheck - now;
|
||||
holder.textInterval.setText(TimeUtil.str(difference));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mServers.size();
|
||||
}
|
||||
|
||||
public static class ServerVH extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
|
||||
|
||||
final StatusImageView iconStatus;
|
||||
final TextView textName;
|
||||
final TextView textInterval;
|
||||
final TextView textUrl;
|
||||
final TextView textStatus;
|
||||
final ServerAdapter adapter;
|
||||
|
||||
public ServerVH(View itemView, ServerAdapter adapter) {
|
||||
super(itemView);
|
||||
iconStatus = (StatusImageView) itemView.findViewById(R.id.iconStatus);
|
||||
textName = (TextView) itemView.findViewById(R.id.textName);
|
||||
textInterval = (TextView) itemView.findViewById(R.id.textInterval);
|
||||
textUrl = (TextView) itemView.findViewById(R.id.textUrl);
|
||||
textStatus = (TextView) itemView.findViewById(R.id.textStatus);
|
||||
this.adapter = adapter;
|
||||
|
||||
itemView.setOnClickListener(this);
|
||||
itemView.setOnLongClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
adapter.performClick(getAdapterPosition(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
adapter.performClick(getAdapterPosition(), true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
131
app/src/main/java/com/afollestad/nocknock/adapter/SiteAdapter.kt
Normal file
131
app/src/main/java/com/afollestad/nocknock/adapter/SiteAdapter.kt
Normal file
|
@ -0,0 +1,131 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil.calculateDiff
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
import com.afollestad.nocknock.data.model.isPending
|
||||
import com.afollestad.nocknock.data.model.textRes
|
||||
import com.afollestad.nocknock.utilities.ui.onDebouncedClick
|
||||
import kotlinx.android.synthetic.main.list_item_server.view.iconStatus
|
||||
import kotlinx.android.synthetic.main.list_item_server.view.textInterval
|
||||
import kotlinx.android.synthetic.main.list_item_server.view.textName
|
||||
import kotlinx.android.synthetic.main.list_item_server.view.textStatus
|
||||
import kotlinx.android.synthetic.main.list_item_server.view.textUrl
|
||||
|
||||
typealias Listener = (model: Site, longClick: Boolean) -> Unit
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class SiteViewHolder constructor(
|
||||
itemView: View,
|
||||
private val adapter: SiteAdapter
|
||||
) : RecyclerView.ViewHolder(itemView), View.OnLongClickListener {
|
||||
|
||||
init {
|
||||
itemView.onDebouncedClick {
|
||||
adapter.performClick(adapterPosition, false)
|
||||
}
|
||||
itemView.setOnLongClickListener(this)
|
||||
}
|
||||
|
||||
fun bind(model: Site) {
|
||||
requireNotNull(model.settings) { "Settings must be populated." }
|
||||
|
||||
itemView.textName.text = model.name
|
||||
itemView.textUrl.text = model.url
|
||||
|
||||
val lastResult = model.lastResult
|
||||
if (lastResult != null) {
|
||||
itemView.iconStatus.setStatus(lastResult.status)
|
||||
val statusText = lastResult.status.textRes()
|
||||
if (statusText == 0) {
|
||||
itemView.textStatus.text = lastResult.reason
|
||||
} else {
|
||||
itemView.textStatus.setText(statusText)
|
||||
}
|
||||
} else {
|
||||
itemView.iconStatus.setStatus(WAITING)
|
||||
itemView.textStatus.setText(R.string.none)
|
||||
}
|
||||
|
||||
val res = itemView.resources
|
||||
when {
|
||||
model.settings?.disabled == true -> {
|
||||
itemView.textInterval.setText(R.string.checks_disabled)
|
||||
}
|
||||
model.lastResult?.status.isPending() -> {
|
||||
itemView.textInterval.text = res.getString(
|
||||
R.string.next_check_x,
|
||||
res.getString(R.string.now)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
itemView.textInterval.text = res.getString(
|
||||
R.string.next_check_x,
|
||||
model.intervalText()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongClick(view: View): Boolean {
|
||||
adapter.performClick(adapterPosition, true)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class SiteAdapter(private val listener: Listener) : RecyclerView.Adapter<SiteViewHolder>() {
|
||||
|
||||
private var models = mutableListOf<Site>()
|
||||
|
||||
internal fun performClick(
|
||||
index: Int,
|
||||
longClick: Boolean
|
||||
) = listener.invoke(models[index], longClick)
|
||||
|
||||
fun set(newModels: List<Site>) {
|
||||
val formerModels = this.models
|
||||
this.models = newModels.toMutableList()
|
||||
val diffResult = calculateDiff(SiteDiffCallback(formerModels, this.models))
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): SiteViewHolder {
|
||||
val v = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.list_item_server, parent, false)
|
||||
return SiteViewHolder(v, this)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: SiteViewHolder,
|
||||
position: Int
|
||||
) {
|
||||
val model = models[position]
|
||||
holder.bind(model)
|
||||
}
|
||||
|
||||
override fun getItemCount() = models.size
|
||||
}
|
|
@ -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
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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package com.afollestad.nocknock.api;
|
||||
|
||||
import com.afollestad.inquiry.annotations.Column;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
public class ServerModel implements Serializable {
|
||||
|
||||
public ServerModel() {
|
||||
}
|
||||
|
||||
@Column(name = "_id", primaryKey = true, notNull = true, autoIncrement = true)
|
||||
public long id;
|
||||
@Column
|
||||
public String name;
|
||||
@Column
|
||||
public String url;
|
||||
@Column
|
||||
@ServerStatus.Enum
|
||||
public int status;
|
||||
@Column
|
||||
public long checkInterval;
|
||||
@Column
|
||||
public long lastCheck;
|
||||
@Column
|
||||
public String reason;
|
||||
|
||||
@Column
|
||||
@ValidationMode.Enum
|
||||
public int validationMode;
|
||||
@Column
|
||||
public String validationContent;
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package com.afollestad.nocknock.api;
|
||||
|
||||
import android.support.annotation.IntDef;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
public final class ServerStatus {
|
||||
|
||||
public final static int OK = 1;
|
||||
public final static int WAITING = 2;
|
||||
public final static int CHECKING = 3;
|
||||
public final static int ERROR = 4;
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({OK, WAITING, CHECKING, ERROR})
|
||||
public @interface Enum {}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package com.afollestad.nocknock.api;
|
||||
|
||||
import android.support.annotation.IntDef;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
public final class ValidationMode {
|
||||
|
||||
public final static int STATUS_CODE = 1;
|
||||
public final static int TERM_SEARCH = 2;
|
||||
public final static int JAVASCRIPT = 3;
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({STATUS_CODE, TERM_SEARCH, JAVASCRIPT})
|
||||
public @interface Enum {
|
||||
}
|
||||
}
|
|
@ -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,33 +0,0 @@
|
|||
package com.afollestad.nocknock.dialogs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.Html;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.afollestad.nocknock.R;
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
public class AboutDialog extends DialogFragment {
|
||||
|
||||
public static void show(AppCompatActivity context) {
|
||||
AboutDialog dialog = new AboutDialog();
|
||||
dialog.show(context.getSupportFragmentManager(), "[ABOUT_DIALOG]");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
return new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.about)
|
||||
.positiveText(R.string.dismiss)
|
||||
.content(Html.fromHtml(getString(R.string.about_body)))
|
||||
.contentLineSpacing(1.6f)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* 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
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.nocknock.BuildConfig
|
||||
import com.afollestad.nocknock.R
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class AboutDialog : DialogFragment() {
|
||||
companion object {
|
||||
private const val TAG = "[ABOUT_DIALOG]"
|
||||
|
||||
fun show(context: AppCompatActivity) {
|
||||
val dialog = AboutDialog()
|
||||
dialog.show(context.supportFragmentManager, TAG)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val context = activity ?: throw IllegalStateException("Oh no!")
|
||||
return MaterialDialog(context)
|
||||
.title(text = getString(R.string.app_name_x, BuildConfig.VERSION_NAME))
|
||||
.positiveButton(R.string.dismiss)
|
||||
.message(R.string.about_body, html = true, lineHeightMultiplier = 1.4f)
|
||||
}
|
||||
}
|
72
app/src/main/java/com/afollestad/nocknock/koin/MainModule.kt
Normal file
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package com.afollestad.nocknock.receivers;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import com.afollestad.inquiry.Inquiry;
|
||||
import com.afollestad.nocknock.api.ServerModel;
|
||||
import com.afollestad.nocknock.ui.MainActivity;
|
||||
import com.afollestad.nocknock.util.AlarmUtil;
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
public class BootReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {
|
||||
final Inquiry inq = Inquiry.newInstance(context, MainActivity.DB_NAME).build(false);
|
||||
ServerModel[] models = inq
|
||||
.selectFrom(MainActivity.SITES_TABLE_NAME, ServerModel.class)
|
||||
.all();
|
||||
AlarmUtil.setSiteChecks(context, models);
|
||||
inq.destroyInstance();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package com.afollestad.nocknock.receivers;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import com.afollestad.nocknock.services.CheckService;
|
||||
import com.afollestad.nocknock.util.NetworkUtil;
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
public class ConnectivityReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
final boolean hasInternet = NetworkUtil.hasInternet(context);
|
||||
Log.v("ConnectivityReceiver", "Connectivity state changed... has internet? " + hasInternet);
|
||||
if (hasInternet) {
|
||||
context.startService(new Intent(context, CheckService.class)
|
||||
.putExtra(CheckService.ONLY_WAITING, true));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,241 +0,0 @@
|
|||
package com.afollestad.nocknock.services;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.afollestad.bridge.Bridge;
|
||||
import com.afollestad.bridge.BridgeException;
|
||||
import com.afollestad.bridge.Response;
|
||||
import com.afollestad.inquiry.Inquiry;
|
||||
import com.afollestad.inquiry.Query;
|
||||
import com.afollestad.nocknock.BuildConfig;
|
||||
import com.afollestad.nocknock.R;
|
||||
import com.afollestad.nocknock.api.ServerModel;
|
||||
import com.afollestad.nocknock.api.ServerStatus;
|
||||
import com.afollestad.nocknock.api.ValidationMode;
|
||||
import com.afollestad.nocknock.ui.MainActivity;
|
||||
import com.afollestad.nocknock.ui.ViewSiteActivity;
|
||||
import com.afollestad.nocknock.util.JsUtil;
|
||||
import com.afollestad.nocknock.util.NetworkUtil;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
@SuppressWarnings("CheckResult")
|
||||
public class CheckService extends IntentService {
|
||||
|
||||
public static String ACTION_CHECK_UPDATE = BuildConfig.APPLICATION_ID + ".CHECK_UPDATE";
|
||||
public static String ACTION_RUNNING = BuildConfig.APPLICATION_ID + ".CHECK_RUNNING";
|
||||
public static String MODEL_ID = "model_id";
|
||||
public static String ONLY_WAITING = "only_waiting";
|
||||
public static int NOTI_ID = 3456;
|
||||
|
||||
public CheckService() {
|
||||
super("NockNockCheckService");
|
||||
}
|
||||
|
||||
private static void LOG(String msg, Object... format) {
|
||||
if (format != null)
|
||||
msg = String.format(Locale.getDefault(), msg, format);
|
||||
Log.v("NockNockService", msg);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
Inquiry.newInstance(this, MainActivity.DB_NAME).build();
|
||||
isRunning(true);
|
||||
Bridge.config()
|
||||
.defaultHeader("User-Agent", getString(R.string.app_name) + " (Android)");
|
||||
|
||||
final Query<ServerModel, Integer> query = Inquiry.get(this)
|
||||
.selectFrom(MainActivity.SITES_TABLE_NAME, ServerModel.class);
|
||||
if (intent != null && intent.hasExtra(MODEL_ID)) {
|
||||
query.where("_id = ?", intent.getLongExtra(MODEL_ID, -1));
|
||||
} else if (intent != null && intent.getBooleanExtra(ONLY_WAITING, false)) {
|
||||
query.where("status = ?", ServerStatus.WAITING);
|
||||
}
|
||||
final ServerModel[] sites = query.all();
|
||||
|
||||
if (sites == null || sites.length == 0) {
|
||||
LOG("No sites added to check, service will terminate.");
|
||||
isRunning(false);
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
|
||||
LOG("Checking %d sites...", sites.length);
|
||||
sendBroadcast(new Intent(ACTION_RUNNING));
|
||||
|
||||
for (ServerModel site : sites) {
|
||||
LOG("Updating %s (%s) status to WAITING...", site.name, site.url);
|
||||
site.status = ServerStatus.WAITING;
|
||||
updateStatus(site);
|
||||
}
|
||||
|
||||
if (NetworkUtil.hasInternet(this)) {
|
||||
for (ServerModel site : sites) {
|
||||
LOG("Checking %s (%s)...", site.name, site.url);
|
||||
site.status = ServerStatus.CHECKING;
|
||||
site.lastCheck = System.currentTimeMillis();
|
||||
updateStatus(site);
|
||||
|
||||
try {
|
||||
final Response response = Bridge.get(site.url)
|
||||
.throwIfNotSuccess()
|
||||
.cancellable(false)
|
||||
.request()
|
||||
.response();
|
||||
|
||||
site.reason = null;
|
||||
site.status = ServerStatus.OK;
|
||||
|
||||
if (site.validationMode == ValidationMode.TERM_SEARCH) {
|
||||
final String body = response.asString();
|
||||
if (body == null || !body.contains(site.validationContent)) {
|
||||
site.status = ServerStatus.ERROR;
|
||||
site.reason = "Term \"" + site.validationContent + "\" not found in response body.";
|
||||
}
|
||||
} else if (site.validationMode == ValidationMode.JAVASCRIPT) {
|
||||
final String body = response.asString();
|
||||
site.reason = JsUtil.exec(site.validationContent, body);
|
||||
if (site.reason != null && !site.toString().isEmpty())
|
||||
site.status = ServerStatus.ERROR;
|
||||
}
|
||||
|
||||
if (site.status == ServerStatus.ERROR)
|
||||
showNotification(this, site);
|
||||
} catch (BridgeException e) {
|
||||
processError(e, site);
|
||||
}
|
||||
updateStatus(site);
|
||||
}
|
||||
} else {
|
||||
LOG("No internet connection, waiting.");
|
||||
}
|
||||
|
||||
isRunning(false);
|
||||
LOG("Service is finished!");
|
||||
}
|
||||
|
||||
private void processError(BridgeException e, ServerModel site) {
|
||||
site.status = ServerStatus.OK;
|
||||
site.reason = null;
|
||||
|
||||
switch (e.reason()) {
|
||||
case BridgeException.REASON_REQUEST_CANCELLED:
|
||||
// Shouldn't happen
|
||||
break;
|
||||
case BridgeException.REASON_REQUEST_FAILED:
|
||||
case BridgeException.REASON_RESPONSE_UNPARSEABLE:
|
||||
case BridgeException.REASON_RESPONSE_UNSUCCESSFUL:
|
||||
case BridgeException.REASON_RESPONSE_IOERROR:
|
||||
//noinspection ConstantConditions
|
||||
if (e.response() != null && e.response().code() == 401) {
|
||||
// Don't consider 401 unsuccessful here
|
||||
site.reason = null;
|
||||
} else {
|
||||
site.status = ServerStatus.ERROR;
|
||||
site.reason = e.getMessage();
|
||||
}
|
||||
break;
|
||||
case BridgeException.REASON_REQUEST_TIMEOUT:
|
||||
site.status = ServerStatus.ERROR;
|
||||
site.reason = getString(R.string.timeout);
|
||||
break;
|
||||
case BridgeException.REASON_RESPONSE_VALIDATOR_ERROR:
|
||||
case BridgeException.REASON_RESPONSE_VALIDATOR_FALSE:
|
||||
// Not used
|
||||
break;
|
||||
}
|
||||
|
||||
if (site.status != ServerStatus.OK) {
|
||||
LOG("%s error: %s", site.name, site.reason);
|
||||
showNotification(this, site);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStatus(ServerModel site) {
|
||||
Inquiry.get(this)
|
||||
.update(MainActivity.SITES_TABLE_NAME, ServerModel.class)
|
||||
.where("_id = ?", site.id)
|
||||
.values(site)
|
||||
.run();
|
||||
sendBroadcast(new Intent(ACTION_CHECK_UPDATE)
|
||||
.putExtra("model", site));
|
||||
}
|
||||
|
||||
private void isRunning(boolean running) {
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.edit().putBoolean("check_service_running", running).commit();
|
||||
}
|
||||
|
||||
public static boolean isRunning(Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean("check_service_running", false);
|
||||
}
|
||||
|
||||
public static void isAppOpen(Context context, boolean open) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit().putBoolean("is_app_open", open).commit();
|
||||
}
|
||||
|
||||
public static boolean isAppOpen(Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean("is_app_open", false);
|
||||
}
|
||||
|
||||
private static void showNotification(Context context, ServerModel site) {
|
||||
if (isAppOpen(context)) {
|
||||
// Don't show notifications while the app is open
|
||||
return;
|
||||
}
|
||||
|
||||
final NotificationManagerCompat nm = NotificationManagerCompat.from(context);
|
||||
final PendingIntent openIntent = PendingIntent.getActivity(context, 9669,
|
||||
new Intent(context, ViewSiteActivity.class)
|
||||
.putExtra("model", site)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
|
||||
PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
final Notification noti = new NotificationCompat.Builder(context)
|
||||
.setContentTitle(site.name)
|
||||
.setContentText(context.getString(R.string.something_wrong))
|
||||
.setContentIntent(openIntent)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher))
|
||||
.setPriority(Notification.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
.setDefaults(Notification.DEFAULT_VIBRATE)
|
||||
.build();
|
||||
nm.notify(site.url, NOTI_ID, noti);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
try {
|
||||
Inquiry.destroy(this);
|
||||
} catch (Throwable t2) {
|
||||
t2.printStackTrace();
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
|
@ -1,255 +0,0 @@
|
|||
package com.afollestad.nocknock.ui;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.TextInputLayout;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Patterns;
|
||||
import android.view.View;
|
||||
import android.view.ViewAnimationUtils;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.afollestad.nocknock.R;
|
||||
import com.afollestad.nocknock.api.ServerModel;
|
||||
import com.afollestad.nocknock.api.ServerStatus;
|
||||
import com.afollestad.nocknock.api.ValidationMode;
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
public class AddSiteActivity extends AppCompatActivity implements View.OnClickListener {
|
||||
|
||||
private View rootLayout;
|
||||
private Toolbar toolbar;
|
||||
|
||||
private TextInputLayout nameTiLayout;
|
||||
private EditText inputName;
|
||||
private TextInputLayout urlTiLayout;
|
||||
private EditText inputUrl;
|
||||
private EditText inputInterval;
|
||||
private Spinner spinnerInterval;
|
||||
private TextView textUrlWarning;
|
||||
private Spinner responseValidationSpinner;
|
||||
|
||||
private boolean isClosing;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_addsite);
|
||||
|
||||
rootLayout = findViewById(R.id.rootView);
|
||||
nameTiLayout = (TextInputLayout) findViewById(R.id.nameTiLayout);
|
||||
inputName = (EditText) findViewById(R.id.inputName);
|
||||
urlTiLayout = (TextInputLayout) findViewById(R.id.urlTiLayout);
|
||||
inputUrl = (EditText) findViewById(R.id.inputUrl);
|
||||
textUrlWarning = (TextView) findViewById(R.id.textUrlWarning);
|
||||
inputInterval = (EditText) findViewById(R.id.checkIntervalInput);
|
||||
spinnerInterval = (Spinner) findViewById(R.id.checkIntervalSpinner);
|
||||
responseValidationSpinner = (Spinner) findViewById(R.id.responseValidationMode);
|
||||
|
||||
toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
toolbar.setNavigationOnClickListener(view -> closeActivityWithReveal());
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
rootLayout.setVisibility(View.INVISIBLE);
|
||||
ViewTreeObserver viewTreeObserver = rootLayout.getViewTreeObserver();
|
||||
if (viewTreeObserver.isAlive()) {
|
||||
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
circularRevealActivity();
|
||||
rootLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ArrayAdapter<String> intervalOptionsAdapter = new ArrayAdapter<>(this, R.layout.list_item_spinner,
|
||||
getResources().getStringArray(R.array.interval_options));
|
||||
intervalOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown);
|
||||
spinnerInterval.setAdapter(intervalOptionsAdapter);
|
||||
|
||||
inputUrl.setOnFocusChangeListener((view, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
final String inputStr = inputUrl.getText().toString().trim();
|
||||
if (inputStr.isEmpty()) return;
|
||||
final Uri uri = Uri.parse(inputStr);
|
||||
if (uri.getScheme() == null) {
|
||||
inputUrl.setText("http://" + inputStr);
|
||||
textUrlWarning.setVisibility(View.GONE);
|
||||
} else if (!"http".equals(uri.getScheme()) && !"https".equals(uri.getScheme())) {
|
||||
textUrlWarning.setVisibility(View.VISIBLE);
|
||||
textUrlWarning.setText(R.string.warning_http_url);
|
||||
} else {
|
||||
textUrlWarning.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ArrayAdapter<String> validationOptionsAdapter = new ArrayAdapter<>(this, R.layout.list_item_spinner,
|
||||
getResources().getStringArray(R.array.response_validation_options));
|
||||
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown);
|
||||
responseValidationSpinner.setAdapter(validationOptionsAdapter);
|
||||
responseValidationSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
|
||||
final View searchTerm = findViewById(R.id.responseValidationSearchTerm);
|
||||
final View javascript = findViewById(R.id.responseValidationScript);
|
||||
final TextView modeDesc = (TextView) findViewById(R.id.validationModeDescription);
|
||||
|
||||
searchTerm.setVisibility(i == 1 ? View.VISIBLE : View.GONE);
|
||||
javascript.setVisibility(i == 2 ? View.VISIBLE : View.GONE);
|
||||
|
||||
switch (i) {
|
||||
case 0:
|
||||
modeDesc.setText(R.string.validation_mode_status_desc);
|
||||
break;
|
||||
case 1:
|
||||
modeDesc.setText(R.string.validation_mode_term_desc);
|
||||
break;
|
||||
case 2:
|
||||
modeDesc.setText(R.string.validation_mode_javascript_desc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> adapterView) {
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.doneBtn).setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
closeActivityWithReveal();
|
||||
}
|
||||
|
||||
private void closeActivityWithReveal() {
|
||||
if (isClosing) return;
|
||||
isClosing = true;
|
||||
final int fabSize = getIntent().getIntExtra("fab_size", toolbar.getMeasuredHeight());
|
||||
final int cx = (int) getIntent().getFloatExtra("fab_x", rootLayout.getMeasuredWidth() / 2) + (fabSize / 2);
|
||||
final int cy = (int) getIntent().getFloatExtra("fab_y", rootLayout.getMeasuredHeight() / 2) + toolbar.getMeasuredHeight() + (fabSize / 2);
|
||||
float initialRadius = Math.max(cx, cy);
|
||||
|
||||
final Animator circularReveal = ViewAnimationUtils.createCircularReveal(rootLayout, cx, cy, initialRadius, 0);
|
||||
circularReveal.setDuration(300);
|
||||
circularReveal.setInterpolator(new AccelerateInterpolator());
|
||||
circularReveal.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
super.onAnimationEnd(animation);
|
||||
rootLayout.setVisibility(View.INVISIBLE);
|
||||
finish();
|
||||
overridePendingTransition(0, 0);
|
||||
}
|
||||
});
|
||||
|
||||
circularReveal.start();
|
||||
}
|
||||
|
||||
private void circularRevealActivity() {
|
||||
final int cx = rootLayout.getMeasuredWidth() / 2;
|
||||
final int cy = rootLayout.getMeasuredHeight() / 2;
|
||||
final float finalRadius = Math.max(cx, cy);
|
||||
final Animator circularReveal = ViewAnimationUtils.createCircularReveal(rootLayout, cx, cy, 0, finalRadius);
|
||||
|
||||
circularReveal.setDuration(300);
|
||||
circularReveal.setInterpolator(new DecelerateInterpolator());
|
||||
|
||||
rootLayout.setVisibility(View.VISIBLE);
|
||||
circularReveal.start();
|
||||
}
|
||||
|
||||
// Done button
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
isClosing = true;
|
||||
|
||||
ServerModel model = new ServerModel();
|
||||
model.name = inputName.getText().toString().trim();
|
||||
model.url = inputUrl.getText().toString().trim();
|
||||
model.status = ServerStatus.WAITING;
|
||||
|
||||
if (model.name.isEmpty()) {
|
||||
nameTiLayout.setError(getString(R.string.please_enter_name));
|
||||
isClosing = false;
|
||||
return;
|
||||
} else {
|
||||
nameTiLayout.setError(null);
|
||||
}
|
||||
|
||||
if (model.url.isEmpty()) {
|
||||
urlTiLayout.setError(getString(R.string.please_enter_url));
|
||||
isClosing = false;
|
||||
return;
|
||||
} else {
|
||||
urlTiLayout.setError(null);
|
||||
if (!Patterns.WEB_URL.matcher(model.url).find()) {
|
||||
urlTiLayout.setError(getString(R.string.please_enter_valid_url));
|
||||
isClosing = false;
|
||||
return;
|
||||
} else {
|
||||
final Uri uri = Uri.parse(model.url);
|
||||
if (uri.getScheme() == null)
|
||||
model.url = "http://" + model.url;
|
||||
}
|
||||
}
|
||||
|
||||
String intervalStr = inputInterval.getText().toString().trim();
|
||||
if (intervalStr.isEmpty()) intervalStr = "0";
|
||||
model.checkInterval = Integer.parseInt(intervalStr);
|
||||
|
||||
switch (spinnerInterval.getSelectedItemPosition()) {
|
||||
case 0: // minutes
|
||||
model.checkInterval *= (60 * 1000);
|
||||
break;
|
||||
case 1: // hours
|
||||
model.checkInterval *= (60 * 60 * 1000);
|
||||
break;
|
||||
case 2: // days
|
||||
model.checkInterval *= (60 * 60 * 24 * 1000);
|
||||
break;
|
||||
default: // weeks
|
||||
model.checkInterval *= (60 * 60 * 24 * 7 * 1000);
|
||||
break;
|
||||
}
|
||||
|
||||
model.lastCheck = System.currentTimeMillis() - model.checkInterval;
|
||||
|
||||
switch (responseValidationSpinner.getSelectedItemPosition()) {
|
||||
case 0:
|
||||
model.validationMode = ValidationMode.STATUS_CODE;
|
||||
model.validationContent = null;
|
||||
break;
|
||||
case 1:
|
||||
model.validationMode = ValidationMode.TERM_SEARCH;
|
||||
model.validationContent = ((EditText) findViewById(R.id.responseValidationSearchTerm)).getText().toString().trim();
|
||||
break;
|
||||
case 2:
|
||||
model.validationMode = ValidationMode.JAVASCRIPT;
|
||||
model.validationContent = ((EditText) findViewById(R.id.responseValidationScriptInput)).getText().toString().trim();
|
||||
break;
|
||||
}
|
||||
|
||||
setResult(RESULT_OK, new Intent()
|
||||
.putExtra("model", model));
|
||||
finish();
|
||||
overridePendingTransition(R.anim.fade_out, R.anim.fade_out);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,326 +0,0 @@
|
|||
package com.afollestad.nocknock.ui;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ActivityOptions;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Path;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.Html;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.animation.PathInterpolator;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.afollestad.bridge.Bridge;
|
||||
import com.afollestad.inquiry.Inquiry;
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.afollestad.nocknock.R;
|
||||
import com.afollestad.nocknock.adapter.ServerAdapter;
|
||||
import com.afollestad.nocknock.api.ServerModel;
|
||||
import com.afollestad.nocknock.api.ValidationMode;
|
||||
import com.afollestad.nocknock.dialogs.AboutDialog;
|
||||
import com.afollestad.nocknock.services.CheckService;
|
||||
import com.afollestad.nocknock.util.AlarmUtil;
|
||||
import com.afollestad.nocknock.util.MathUtil;
|
||||
import com.afollestad.nocknock.views.DividerItemDecoration;
|
||||
|
||||
public class MainActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener, View.OnClickListener, ServerAdapter.ClickListener {
|
||||
|
||||
private final static int ADD_SITE_RQ = 6969;
|
||||
private final static int VIEW_SITE_RQ = 6923;
|
||||
public final static String DB_NAME = "nock_nock";
|
||||
public final static String SITES_TABLE_NAME_OLD = "sites";
|
||||
public final static String SITES_TABLE_NAME = "site_models";
|
||||
|
||||
private FloatingActionButton mFab;
|
||||
private RecyclerView mList;
|
||||
private ServerAdapter mAdapter;
|
||||
private TextView mEmptyText;
|
||||
private SwipeRefreshLayout mRefreshLayout;
|
||||
|
||||
private ObjectAnimator mFabAnimator;
|
||||
private float mOrigFabX;
|
||||
private float mOrigFabY;
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.v("MainActivity", "Received " + intent.getAction());
|
||||
if (CheckService.ACTION_RUNNING.equals(intent.getAction())) {
|
||||
if (mRefreshLayout != null)
|
||||
mRefreshLayout.setRefreshing(false);
|
||||
} else {
|
||||
final ServerModel model = (ServerModel) intent.getSerializableExtra("model");
|
||||
if (mAdapter != null && mList != null && model != null) {
|
||||
mList.post(() -> mAdapter.update(model));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressLint("CommitPrefEdits")
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
mAdapter = new ServerAdapter(this);
|
||||
mEmptyText = (TextView) findViewById(R.id.emptyText);
|
||||
|
||||
mList = (RecyclerView) findViewById(R.id.list);
|
||||
mList.setLayoutManager(new LinearLayoutManager(this));
|
||||
mList.setAdapter(mAdapter);
|
||||
mList.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
|
||||
|
||||
mRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefreshLayout);
|
||||
mRefreshLayout.setOnRefreshListener(this);
|
||||
mRefreshLayout.setColorSchemeColors(ContextCompat.getColor(this, R.color.md_green),
|
||||
ContextCompat.getColor(this, R.color.md_yellow),
|
||||
ContextCompat.getColor(this, R.color.md_red));
|
||||
|
||||
mFab = (FloatingActionButton) findViewById(R.id.fab);
|
||||
mFab.setOnClickListener(this);
|
||||
|
||||
Inquiry.newInstance(this, DB_NAME).build();
|
||||
Bridge.config()
|
||||
.defaultHeader("User-Agent", getString(R.string.app_name) + " (Android)");
|
||||
|
||||
final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
if (!sp.getBoolean("migrated_db", false)) {
|
||||
final Inquiry mdb = Inquiry.newInstance(this, DB_NAME)
|
||||
.instanceName("migrate_db")
|
||||
.build(false);
|
||||
final ServerModel[] models = Inquiry.get(this)
|
||||
.selectFrom(SITES_TABLE_NAME_OLD, ServerModel.class)
|
||||
.projection("_id", "name", "url", "status", "checkInterval", "lastCheck", "reason")
|
||||
.all();
|
||||
if (models != null) {
|
||||
Log.d("SiteMigration", "Migrating " + models.length + " sites to the new table.");
|
||||
for (ServerModel model : models) {
|
||||
model.validationMode = ValidationMode.STATUS_CODE;
|
||||
model.validationContent = null;
|
||||
}
|
||||
//noinspection CheckResult
|
||||
mdb.insertInto(SITES_TABLE_NAME, ServerModel.class)
|
||||
.values(models)
|
||||
.run();
|
||||
mdb.dropTable(SITES_TABLE_NAME_OLD);
|
||||
}
|
||||
sp.edit().putBoolean("migrated_db", true).commit();
|
||||
}
|
||||
}
|
||||
|
||||
private void showRefreshTutorial() {
|
||||
if (mAdapter.getItemCount() == 0) return;
|
||||
final SharedPreferences pr = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
if (pr.getBoolean("shown_swipe_refresh_tutorial", false)) return;
|
||||
|
||||
mFab.hide();
|
||||
final View tutorialView = findViewById(R.id.swipeRefreshTutorial);
|
||||
tutorialView.setVisibility(View.VISIBLE);
|
||||
tutorialView.setAlpha(0f);
|
||||
tutorialView.animate().cancel();
|
||||
tutorialView.animate().setDuration(300).alpha(1f).start();
|
||||
|
||||
findViewById(R.id.understoodBtn).setOnClickListener(view -> {
|
||||
view.setOnClickListener(null);
|
||||
findViewById(R.id.swipeRefreshTutorial).setVisibility(View.GONE);
|
||||
pr.edit().putBoolean("shown_swipe_refresh_tutorial", true).commit();
|
||||
mFab.show();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
CheckService.isAppOpen(this, true);
|
||||
|
||||
try {
|
||||
final IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(CheckService.ACTION_CHECK_UPDATE);
|
||||
filter.addAction(CheckService.ACTION_RUNNING);
|
||||
registerReceiver(mReceiver, filter);
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
|
||||
refreshModels();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
CheckService.isAppOpen(this, false);
|
||||
|
||||
if (isFinishing()) {
|
||||
Inquiry.destroy(this);
|
||||
}
|
||||
|
||||
NotificationManagerCompat.from(this).cancel(CheckService.NOTI_ID);
|
||||
try {
|
||||
unregisterReceiver(mReceiver);
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshModels() {
|
||||
mAdapter.clear();
|
||||
mEmptyText.setVisibility(View.VISIBLE);
|
||||
Inquiry.get(this)
|
||||
.selectFrom(SITES_TABLE_NAME, ServerModel.class)
|
||||
.all(this::setModels);
|
||||
}
|
||||
|
||||
private void setModels(ServerModel[] models) {
|
||||
mAdapter.set(models);
|
||||
mEmptyText.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
|
||||
AlarmUtil.setSiteChecks(this, models);
|
||||
showRefreshTutorial();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.about) {
|
||||
AboutDialog.show(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
if (CheckService.isRunning(this)) {
|
||||
mRefreshLayout.setRefreshing(false);
|
||||
return;
|
||||
}
|
||||
startService(new Intent(this, CheckService.class));
|
||||
}
|
||||
|
||||
// FAB clicked
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
mOrigFabX = mFab.getX();
|
||||
mOrigFabY = mFab.getY();
|
||||
final Path curve = MathUtil.bezierCurve(mFab, mList);
|
||||
if (mFabAnimator != null)
|
||||
mFabAnimator.cancel();
|
||||
mFabAnimator = ObjectAnimator.ofFloat(view, View.X, View.Y, curve);
|
||||
mFabAnimator.setInterpolator(new PathInterpolator(.5f, .5f));
|
||||
mFabAnimator.setDuration(300);
|
||||
mFabAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
super.onAnimationEnd(animation);
|
||||
startActivityForResult(new Intent(MainActivity.this, AddSiteActivity.class)
|
||||
.putExtra("fab_x", mOrigFabX)
|
||||
.putExtra("fab_y", mOrigFabY)
|
||||
.putExtra("fab_size", mFab.getMeasuredWidth())
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION), ADD_SITE_RQ);
|
||||
mFab.postDelayed(() -> {
|
||||
mFab.setX(mOrigFabX);
|
||||
mFab.setY(mOrigFabY);
|
||||
}, 600);
|
||||
}
|
||||
});
|
||||
mFabAnimator.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (resultCode == RESULT_OK) {
|
||||
final ServerModel model = (ServerModel) data.getSerializableExtra("model");
|
||||
if (requestCode == ADD_SITE_RQ) {
|
||||
mAdapter.add(model);
|
||||
mEmptyText.setVisibility(View.GONE);
|
||||
Inquiry.get(this).insertInto(SITES_TABLE_NAME, ServerModel.class)
|
||||
.values(model)
|
||||
.run(inserted -> {
|
||||
AlarmUtil.setSiteChecks(MainActivity.this, model);
|
||||
checkSite(MainActivity.this, model);
|
||||
});
|
||||
} else if (requestCode == VIEW_SITE_RQ) {
|
||||
mAdapter.update(model);
|
||||
AlarmUtil.setSiteChecks(MainActivity.this, model);
|
||||
checkSite(MainActivity.this, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeSite(final Context context, final ServerModel model, final Runnable onRemoved) {
|
||||
new MaterialDialog.Builder(context)
|
||||
.title(R.string.remove_site)
|
||||
.content(Html.fromHtml(context.getString(R.string.remove_site_prompt, model.name)))
|
||||
.positiveText(R.string.remove)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive((dialog, which) -> {
|
||||
AlarmUtil.cancelSiteChecks(context, model);
|
||||
final NotificationManagerCompat nm = NotificationManagerCompat.from(context);
|
||||
nm.cancel(model.url, CheckService.NOTI_ID);
|
||||
//noinspection CheckResult
|
||||
final Inquiry rinq = Inquiry.newInstance(context, DB_NAME)
|
||||
.instanceName("remove_site")
|
||||
.build(false);
|
||||
//noinspection CheckResult
|
||||
rinq.deleteFrom(SITES_TABLE_NAME, ServerModel.class)
|
||||
.where("_id = ?", model.id)
|
||||
.run();
|
||||
rinq.destroyInstance();
|
||||
if (onRemoved != null)
|
||||
onRemoved.run();
|
||||
}).show();
|
||||
}
|
||||
|
||||
public static void checkSite(Context context, ServerModel model) {
|
||||
context.startService(new Intent(context, CheckService.class)
|
||||
.putExtra(CheckService.MODEL_ID, model.id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSiteSelected(final int index, final ServerModel model, boolean longClick) {
|
||||
if (longClick) {
|
||||
new MaterialDialog.Builder(this)
|
||||
.title(R.string.options)
|
||||
.items(R.array.site_long_options)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.itemsCallback((dialog, itemView, which, text) -> {
|
||||
if (which == 0) {
|
||||
checkSite(MainActivity.this, model);
|
||||
} else {
|
||||
removeSite(MainActivity.this, model, () -> {
|
||||
mAdapter.remove(index);
|
||||
mEmptyText.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
|
||||
});
|
||||
}
|
||||
}).show();
|
||||
} else {
|
||||
startActivityForResult(new Intent(this, ViewSiteActivity.class)
|
||||
.putExtra("model", model), VIEW_SITE_RQ,
|
||||
ActivityOptions.makeSceneTransitionAnimation(this).toBundle());
|
||||
}
|
||||
}
|
||||
}
|
26
app/src/main/java/com/afollestad/nocknock/ui/NightMode.kt
Normal file
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,340 +0,0 @@
|
|||
package com.afollestad.nocknock.ui;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.util.Patterns;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.afollestad.bridge.Bridge;
|
||||
import com.afollestad.inquiry.Inquiry;
|
||||
import com.afollestad.nocknock.R;
|
||||
import com.afollestad.nocknock.api.ServerModel;
|
||||
import com.afollestad.nocknock.api.ServerStatus;
|
||||
import com.afollestad.nocknock.api.ValidationMode;
|
||||
import com.afollestad.nocknock.services.CheckService;
|
||||
import com.afollestad.nocknock.util.TimeUtil;
|
||||
import com.afollestad.nocknock.views.StatusImageView;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
public class ViewSiteActivity extends AppCompatActivity implements View.OnClickListener, Toolbar.OnMenuItemClickListener {
|
||||
|
||||
private StatusImageView iconStatus;
|
||||
private EditText inputName;
|
||||
private EditText inputUrl;
|
||||
private EditText inputCheckInterval;
|
||||
private Spinner checkIntervalSpinner;
|
||||
private TextView textLastCheckResult;
|
||||
private TextView textNextCheck;
|
||||
private TextView textUrlWarning;
|
||||
private Spinner responseValidationSpinner;
|
||||
|
||||
private ServerModel mModel;
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.v("ViewSiteActivity", "Received " + intent.getAction());
|
||||
final ServerModel model = (ServerModel) intent.getSerializableExtra("model");
|
||||
if (model != null) {
|
||||
mModel = model;
|
||||
update();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_viewsite);
|
||||
|
||||
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
toolbar.setNavigationOnClickListener(view -> finish());
|
||||
toolbar.inflateMenu(R.menu.menu_viewsite);
|
||||
toolbar.setOnMenuItemClickListener(this);
|
||||
|
||||
iconStatus = (StatusImageView) findViewById(R.id.iconStatus);
|
||||
inputName = (EditText) findViewById(R.id.inputName);
|
||||
inputUrl = (EditText) findViewById(R.id.inputUrl);
|
||||
textUrlWarning = (TextView) findViewById(R.id.textUrlWarning);
|
||||
inputCheckInterval = (EditText) findViewById(R.id.checkIntervalInput);
|
||||
checkIntervalSpinner = (Spinner) findViewById(R.id.checkIntervalSpinner);
|
||||
textLastCheckResult = (TextView) findViewById(R.id.textLastCheckResult);
|
||||
textNextCheck = (TextView) findViewById(R.id.textNextCheck);
|
||||
responseValidationSpinner = (Spinner) findViewById(R.id.responseValidationMode);
|
||||
|
||||
ArrayAdapter<String> intervalOptionsAdapter = new ArrayAdapter<>(this, R.layout.list_item_spinner,
|
||||
getResources().getStringArray(R.array.interval_options));
|
||||
intervalOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown);
|
||||
checkIntervalSpinner.setAdapter(intervalOptionsAdapter);
|
||||
|
||||
inputUrl.setOnFocusChangeListener((view, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
final String inputStr = inputUrl.getText().toString().trim();
|
||||
if (inputStr.isEmpty()) return;
|
||||
final Uri uri = Uri.parse(inputStr);
|
||||
if (uri.getScheme() == null) {
|
||||
inputUrl.setText("http://" + inputStr);
|
||||
textUrlWarning.setVisibility(View.GONE);
|
||||
} else if (!"http".equals(uri.getScheme()) && !"https".equals(uri.getScheme())) {
|
||||
textUrlWarning.setVisibility(View.VISIBLE);
|
||||
textUrlWarning.setText(R.string.warning_http_url);
|
||||
} else {
|
||||
textUrlWarning.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ArrayAdapter<String> validationOptionsAdapter = new ArrayAdapter<>(this, R.layout.list_item_spinner,
|
||||
getResources().getStringArray(R.array.response_validation_options));
|
||||
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown);
|
||||
responseValidationSpinner.setAdapter(validationOptionsAdapter);
|
||||
responseValidationSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
|
||||
final View searchTerm = findViewById(R.id.responseValidationSearchTerm);
|
||||
final View javascript = findViewById(R.id.responseValidationScript);
|
||||
final TextView modeDesc = (TextView) findViewById(R.id.validationModeDescription);
|
||||
|
||||
searchTerm.setVisibility(i == 1 ? View.VISIBLE : View.GONE);
|
||||
javascript.setVisibility(i == 2 ? View.VISIBLE : View.GONE);
|
||||
|
||||
switch (i) {
|
||||
case 0:
|
||||
modeDesc.setText(R.string.validation_mode_status_desc);
|
||||
break;
|
||||
case 1:
|
||||
modeDesc.setText(R.string.validation_mode_term_desc);
|
||||
break;
|
||||
case 2:
|
||||
modeDesc.setText(R.string.validation_mode_javascript_desc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> adapterView) {
|
||||
}
|
||||
});
|
||||
|
||||
mModel = (ServerModel) getIntent().getSerializableExtra("model");
|
||||
update();
|
||||
|
||||
Bridge.config()
|
||||
.defaultHeader("User-Agent", getString(R.string.app_name) + " (Android)");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
if (intent != null && intent.hasExtra("model")) {
|
||||
mModel = (ServerModel) intent.getSerializableExtra("model");
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint({"SetTextI18n", "SwitchIntDef"})
|
||||
private void update() {
|
||||
final SimpleDateFormat df = new SimpleDateFormat("MMMM dd, hh:mm:ss a", Locale.getDefault());
|
||||
|
||||
iconStatus.setStatus(mModel.status);
|
||||
inputName.setText(mModel.name);
|
||||
inputUrl.setText(mModel.url);
|
||||
|
||||
if (mModel.lastCheck == 0) {
|
||||
textLastCheckResult.setText(R.string.none);
|
||||
} else {
|
||||
switch (mModel.status) {
|
||||
case ServerStatus.CHECKING:
|
||||
textLastCheckResult.setText(R.string.checking_status);
|
||||
break;
|
||||
case ServerStatus.ERROR:
|
||||
textLastCheckResult.setText(mModel.reason);
|
||||
break;
|
||||
case ServerStatus.OK:
|
||||
textLastCheckResult.setText(R.string.everything_checks_out);
|
||||
break;
|
||||
case ServerStatus.WAITING:
|
||||
textLastCheckResult.setText(R.string.waiting);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mModel.checkInterval == 0) {
|
||||
textNextCheck.setText(R.string.none_turned_off);
|
||||
inputCheckInterval.setText("");
|
||||
checkIntervalSpinner.setSelection(0);
|
||||
} else {
|
||||
long lastCheck = mModel.lastCheck;
|
||||
if (lastCheck == 0) lastCheck = System.currentTimeMillis();
|
||||
textNextCheck.setText(df.format(new Date(lastCheck + mModel.checkInterval)));
|
||||
|
||||
if (mModel.checkInterval >= TimeUtil.WEEK) {
|
||||
inputCheckInterval.setText(Integer.toString((int) Math.ceil(((float) mModel.checkInterval / (float) TimeUtil.WEEK))));
|
||||
checkIntervalSpinner.setSelection(3);
|
||||
} else if (mModel.checkInterval >= TimeUtil.DAY) {
|
||||
inputCheckInterval.setText(Integer.toString((int) Math.ceil(((float) mModel.checkInterval / (float) TimeUtil.DAY))));
|
||||
checkIntervalSpinner.setSelection(2);
|
||||
} else if (mModel.checkInterval >= TimeUtil.HOUR) {
|
||||
inputCheckInterval.setText(Integer.toString((int) Math.ceil(((float) mModel.checkInterval / (float) TimeUtil.HOUR))));
|
||||
checkIntervalSpinner.setSelection(1);
|
||||
} else if (mModel.checkInterval >= TimeUtil.MINUTE) {
|
||||
inputCheckInterval.setText(Integer.toString((int) Math.ceil(((float) mModel.checkInterval / (float) TimeUtil.MINUTE))));
|
||||
checkIntervalSpinner.setSelection(0);
|
||||
} else {
|
||||
inputCheckInterval.setText("0");
|
||||
checkIntervalSpinner.setSelection(0);
|
||||
}
|
||||
}
|
||||
|
||||
responseValidationSpinner.setSelection(mModel.validationMode - 1);
|
||||
switch (mModel.validationMode) {
|
||||
case ValidationMode.TERM_SEARCH:
|
||||
((TextView) findViewById(R.id.responseValidationSearchTerm)).setText(mModel.validationContent);
|
||||
break;
|
||||
case ValidationMode.JAVASCRIPT:
|
||||
((TextView) findViewById(R.id.responseValidationScriptInput)).setText(mModel.validationContent);
|
||||
break;
|
||||
}
|
||||
|
||||
findViewById(R.id.doneBtn).setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
try {
|
||||
final IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(CheckService.ACTION_CHECK_UPDATE);
|
||||
// filter.addAction(CheckService.ACTION_RUNNING);
|
||||
registerReceiver(mReceiver, filter);
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
try {
|
||||
unregisterReceiver(mReceiver);
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void performSave(boolean withValidation) {
|
||||
mModel.name = inputName.getText().toString().trim();
|
||||
mModel.url = inputUrl.getText().toString().trim();
|
||||
mModel.status = ServerStatus.WAITING;
|
||||
|
||||
if (withValidation && mModel.name.isEmpty()) {
|
||||
inputName.setError(getString(R.string.please_enter_name));
|
||||
return;
|
||||
} else {
|
||||
inputName.setError(null);
|
||||
}
|
||||
|
||||
if (withValidation && mModel.url.isEmpty()) {
|
||||
inputUrl.setError(getString(R.string.please_enter_url));
|
||||
return;
|
||||
} else {
|
||||
inputUrl.setError(null);
|
||||
if (withValidation && !Patterns.WEB_URL.matcher(mModel.url).find()) {
|
||||
inputUrl.setError(getString(R.string.please_enter_valid_url));
|
||||
return;
|
||||
} else {
|
||||
final Uri uri = Uri.parse(mModel.url);
|
||||
if (uri.getScheme() == null)
|
||||
mModel.url = "http://" + mModel.url;
|
||||
}
|
||||
}
|
||||
|
||||
String intervalStr = inputCheckInterval.getText().toString().trim();
|
||||
if (intervalStr.isEmpty()) intervalStr = "0";
|
||||
mModel.checkInterval = Integer.parseInt(intervalStr);
|
||||
|
||||
switch (checkIntervalSpinner.getSelectedItemPosition()) {
|
||||
case 0: // minutes
|
||||
mModel.checkInterval *= (60 * 1000);
|
||||
break;
|
||||
case 1: // hours
|
||||
mModel.checkInterval *= (60 * 60 * 1000);
|
||||
break;
|
||||
case 2: // days
|
||||
mModel.checkInterval *= (60 * 60 * 24 * 1000);
|
||||
break;
|
||||
default: // weeks
|
||||
mModel.checkInterval *= (60 * 60 * 24 * 7 * 1000);
|
||||
break;
|
||||
}
|
||||
|
||||
mModel.lastCheck = System.currentTimeMillis() - mModel.checkInterval;
|
||||
|
||||
switch (responseValidationSpinner.getSelectedItemPosition()) {
|
||||
case 0:
|
||||
mModel.validationMode = ValidationMode.STATUS_CODE;
|
||||
mModel.validationContent = null;
|
||||
break;
|
||||
case 1:
|
||||
mModel.validationMode = ValidationMode.TERM_SEARCH;
|
||||
mModel.validationContent = ((EditText) findViewById(R.id.responseValidationSearchTerm)).getText().toString().trim();
|
||||
break;
|
||||
case 2:
|
||||
mModel.validationMode = ValidationMode.JAVASCRIPT;
|
||||
mModel.validationContent = ((EditText) findViewById(R.id.responseValidationScriptInput)).getText().toString().trim();
|
||||
break;
|
||||
}
|
||||
|
||||
final Inquiry inq = Inquiry.newInstance(this, MainActivity.DB_NAME)
|
||||
.build(false);
|
||||
//noinspection CheckResult
|
||||
inq.update(MainActivity.SITES_TABLE_NAME, ServerModel.class)
|
||||
.where("_id = ?", mModel.id)
|
||||
.values(mModel)
|
||||
.run();
|
||||
inq.destroyInstance();
|
||||
}
|
||||
|
||||
// Save button
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
performSave(true);
|
||||
setResult(RESULT_OK, new Intent().putExtra("model", mModel));
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.refresh:
|
||||
performSave(false);
|
||||
MainActivity.checkSite(this, mModel);
|
||||
return true;
|
||||
case R.id.remove:
|
||||
MainActivity.removeSite(this, mModel, this::finish);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,235 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.addsite
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.Intent.ACTION_OPEN_DOCUMENT
|
||||
import android.content.Intent.CATEGORY_OPENABLE
|
||||
import android.os.Bundle
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.lifecycle.Observer
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.ValidationMode
|
||||
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
||||
import com.afollestad.nocknock.ui.viewsite.KEY_SITE
|
||||
import com.afollestad.nocknock.utilities.ext.onTextChanged
|
||||
import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
|
||||
import com.afollestad.nocknock.utilities.livedata.distinct
|
||||
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
|
||||
import com.afollestad.nocknock.viewcomponents.ext.isVisibleCondition
|
||||
import com.afollestad.nocknock.viewcomponents.ext.onScroll
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||
import com.afollestad.vvalidator.form
|
||||
import com.afollestad.vvalidator.form.Form
|
||||
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout
|
||||
import kotlinx.android.synthetic.main.activity_addsite.headersLayout
|
||||
import kotlinx.android.synthetic.main.activity_addsite.inputName
|
||||
import kotlinx.android.synthetic.main.activity_addsite.inputTags
|
||||
import kotlinx.android.synthetic.main.activity_addsite.inputUrl
|
||||
import kotlinx.android.synthetic.main.activity_addsite.loadingProgress
|
||||
import kotlinx.android.synthetic.main.activity_addsite.responseTimeoutInput
|
||||
import kotlinx.android.synthetic.main.activity_addsite.responseValidationMode
|
||||
import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchTerm
|
||||
import kotlinx.android.synthetic.main.activity_addsite.retryPolicyLayout
|
||||
import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout
|
||||
import kotlinx.android.synthetic.main.activity_addsite.scrollView
|
||||
import kotlinx.android.synthetic.main.activity_addsite.sslCertificateBrowse
|
||||
import kotlinx.android.synthetic.main.activity_addsite.sslCertificateInput
|
||||
import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning
|
||||
import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
|
||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import kotlinx.android.synthetic.main.include_app_bar.app_toolbar as appToolbar
|
||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class AddSiteActivity : DarkModeSwitchActivity() {
|
||||
companion object {
|
||||
private const val SELECT_CERT_FILE_RQ = 23
|
||||
}
|
||||
|
||||
private val viewModel by viewModel<AddSiteViewModel>()
|
||||
private lateinit var validationForm: Form
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_addsite)
|
||||
setupUi()
|
||||
setupValidation()
|
||||
|
||||
lifecycle.addObserver(viewModel)
|
||||
|
||||
// Populate view model with initial data
|
||||
val model = intent.getSerializableExtra(KEY_SITE) as? Site
|
||||
model?.let { viewModel.prePopulateFromModel(model) }
|
||||
|
||||
// Loading
|
||||
loadingProgress.observe(this, viewModel.onIsLoading())
|
||||
|
||||
// Name
|
||||
inputName.attachLiveData(this, viewModel.name)
|
||||
|
||||
// Tags
|
||||
inputTags.attachLiveData(this, viewModel.tags)
|
||||
|
||||
// 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(
|
||||
this,
|
||||
R.layout.list_item_spinner,
|
||||
resources.getStringArray(R.array.response_validation_options)
|
||||
)
|
||||
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
||||
responseValidationMode.adapter = validationOptionsAdapter
|
||||
|
||||
scrollView.onScroll {
|
||||
appToolbar.elevation = if (it > appToolbar.measuredHeight / 2) {
|
||||
appToolbar.dimenFloat(R.dimen.default_elevation)
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
|
||||
// SSL certificate
|
||||
sslCertificateBrowse.setOnClickListener {
|
||||
val intent = Intent(ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
}
|
||||
startActivityForResult(intent, SELECT_CERT_FILE_RQ)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupValidation() {
|
||||
validationForm = form {
|
||||
input(inputName, name = "Name") {
|
||||
isNotEmpty().description(R.string.please_enter_name)
|
||||
}
|
||||
input(inputUrl, name = "URL") {
|
||||
isNotEmpty().description(R.string.please_enter_url)
|
||||
isUrl().description(R.string.please_enter_valid_url)
|
||||
}
|
||||
input(responseTimeoutInput, name = "Timeout", optional = true) {
|
||||
isNumber().greaterThan(0)
|
||||
.description(R.string.please_enter_networkTimeout)
|
||||
}
|
||||
input(responseValidationSearchTerm, name = "Search term") {
|
||||
conditional(responseValidationSearchTerm.isVisibleCondition()) {
|
||||
isNotEmpty().description(R.string.please_enter_search_term)
|
||||
}
|
||||
}
|
||||
input(sslCertificateInput, name = "Certificate Path", optional = true) {
|
||||
isUri().hasScheme("file", "content")
|
||||
.that { it.host != null }
|
||||
.description(R.string.please_enter_validCertUri)
|
||||
}
|
||||
submitWith(toolbar.menu, R.id.commit) {
|
||||
viewModel.commit {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validation script
|
||||
scriptInputLayout.attach(
|
||||
codeData = viewModel.validationScript,
|
||||
visibility = viewModel.onValidationScriptVisibility(),
|
||||
form = validationForm
|
||||
)
|
||||
|
||||
// Check interval
|
||||
checkIntervalLayout.attach(
|
||||
valueData = viewModel.checkIntervalValue,
|
||||
multiplierData = viewModel.checkIntervalUnit,
|
||||
form = validationForm
|
||||
)
|
||||
|
||||
// Retry Policy
|
||||
retryPolicyLayout.attach(
|
||||
timesData = viewModel.retryPolicyTimes,
|
||||
minutesData = viewModel.retryPolicyMinutes,
|
||||
form = validationForm
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {
|
||||
appToolbar.dimenFloat(R.dimen.default_elevation)
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
resultData: Intent?
|
||||
) {
|
||||
super.onActivityResult(requestCode, resultCode, resultData)
|
||||
if (requestCode == SELECT_CERT_FILE_RQ && resultCode == RESULT_OK) {
|
||||
sslCertificateInput.setText(resultData?.data?.toString() ?: "")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.main
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.list.listItems
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.adapter.SiteAdapter
|
||||
import com.afollestad.nocknock.adapter.TagAdapter
|
||||
import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.dialogs.AboutDialog
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
||||
import com.afollestad.nocknock.ui.NightMode.UNKNOWN
|
||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||
import kotlinx.android.synthetic.main.activity_main.fab
|
||||
import kotlinx.android.synthetic.main.activity_main.list
|
||||
import kotlinx.android.synthetic.main.activity_main.loadingProgress
|
||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||
import kotlinx.android.synthetic.main.include_empty_view.emptyText
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import kotlinx.android.synthetic.main.activity_main.tags_list as tagsList
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class MainActivity : DarkModeSwitchActivity() {
|
||||
|
||||
private val notificationManager by inject<NockNotificationManager>()
|
||||
private val intentProvider by inject<IntentProvider>()
|
||||
|
||||
internal val viewModel by viewModel<MainViewModel>()
|
||||
|
||||
private lateinit var siteAdapter: SiteAdapter
|
||||
private lateinit var tagAdapter: TagAdapter
|
||||
|
||||
private val statusUpdateReceiver by lazy {
|
||||
StatusUpdateIntentReceiver(application, intentProvider) {
|
||||
viewModel.postSiteUpdate(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
setupUi()
|
||||
|
||||
notificationManager.createChannels()
|
||||
|
||||
lifecycle.run {
|
||||
addObserver(viewModel)
|
||||
addObserver(statusUpdateReceiver)
|
||||
}
|
||||
|
||||
viewModel.onSites()
|
||||
.observe(this, Observer { siteAdapter.set(it) })
|
||||
viewModel.onEmptyTextVisibility()
|
||||
.toViewVisibility(this, emptyText)
|
||||
viewModel.onTags()
|
||||
.observe(this, Observer { tagAdapter.set(it) })
|
||||
viewModel.onTagsListVisibility()
|
||||
.toViewVisibility(this, tagsList)
|
||||
loadingProgress.observe(this, viewModel.onIsLoading())
|
||||
|
||||
processIntent(intent)
|
||||
}
|
||||
|
||||
private fun setupUi() {
|
||||
toolbar.run {
|
||||
inflateMenu(R.menu.menu_main)
|
||||
menu.findItem(R.id.dark_mode)
|
||||
.apply {
|
||||
if (getCurrentNightMode() == UNKNOWN) {
|
||||
isChecked = isDarkMode()
|
||||
} else {
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.about -> AboutDialog.show(this@MainActivity)
|
||||
R.id.dark_mode -> toggleDarkMode()
|
||||
}
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
}
|
||||
|
||||
siteAdapter = SiteAdapter(this::onSiteSelected)
|
||||
list.run {
|
||||
layoutManager = LinearLayoutManager(this@MainActivity)
|
||||
adapter = siteAdapter
|
||||
addItemDecoration(DividerItemDecoration(this@MainActivity, VERTICAL))
|
||||
}
|
||||
|
||||
tagAdapter = TagAdapter(viewModel::onTagSelection)
|
||||
tagsList.run {
|
||||
layoutManager = LinearLayoutManager(this@MainActivity, HORIZONTAL, false)
|
||||
adapter = tagAdapter
|
||||
}
|
||||
|
||||
fab.setOnClickListener { addSite() }
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
intent?.let(::processIntent)
|
||||
}
|
||||
|
||||
private fun onSiteSelected(
|
||||
model: Site,
|
||||
longClick: Boolean
|
||||
) {
|
||||
if (longClick) {
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.options)
|
||||
listItems(R.array.site_long_options) { _, i, _ ->
|
||||
when (i) {
|
||||
0 -> viewModel.refreshSite(model)
|
||||
1 -> addSiteForDuplication(model)
|
||||
2 -> maybeRemoveSite(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
viewSite(model)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.main
|
||||
|
||||
import android.content.Intent
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.toHtml
|
||||
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
||||
import com.afollestad.nocknock.ui.viewsite.KEY_SITE
|
||||
import com.afollestad.nocknock.ui.viewsite.ViewSiteActivity
|
||||
import com.afollestad.nocknock.utilities.providers.RealIntentProvider.Companion.KEY_VIEW_NOTIFICATION_MODEL
|
||||
|
||||
internal const val VIEW_SITE_RQ = 6923
|
||||
internal const val ADD_SITE_RQ = 6969
|
||||
|
||||
// ADD
|
||||
|
||||
internal fun MainActivity.addSite() {
|
||||
startActivityForResult(intentToAdd(), ADD_SITE_RQ)
|
||||
}
|
||||
|
||||
internal fun MainActivity.addSiteForDuplication(site: Site) {
|
||||
startActivityForResult(intentToAdd(site), ADD_SITE_RQ)
|
||||
}
|
||||
|
||||
private fun MainActivity.intentToAdd(model: Site? = null) =
|
||||
Intent(this, AddSiteActivity::class.java).apply {
|
||||
model?.let { putExtra(KEY_SITE, it) }
|
||||
}
|
||||
|
||||
// VIEW
|
||||
|
||||
internal fun MainActivity.viewSite(model: Site) {
|
||||
startActivityForResult(intentToView(model), VIEW_SITE_RQ)
|
||||
}
|
||||
|
||||
private fun MainActivity.intentToView(model: Site) =
|
||||
Intent(this, ViewSiteActivity::class.java).apply {
|
||||
putExtra(KEY_SITE, model)
|
||||
}
|
||||
|
||||
// MISC
|
||||
|
||||
internal fun MainActivity.maybeRemoveSite(model: Site) {
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.remove_site)
|
||||
message(text = context.getString(R.string.remove_site_prompt, model.name).toHtml())
|
||||
positiveButton(R.string.remove) { viewModel.removeSite(model) }
|
||||
negativeButton(android.R.string.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun MainActivity.processIntent(intent: Intent) {
|
||||
if (intent.hasExtra(KEY_VIEW_NOTIFICATION_MODEL)) {
|
||||
val model = intent.getSerializableExtra(KEY_VIEW_NOTIFICATION_MODEL) as Site
|
||||
viewSite(model)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,291 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.viewsite
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.Intent.ACTION_OPEN_DOCUMENT
|
||||
import android.content.Intent.CATEGORY_OPENABLE
|
||||
import android.os.Bundle
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.lifecycle.Observer
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.ValidationMode
|
||||
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
||||
import com.afollestad.nocknock.utilities.ext.onTextChanged
|
||||
import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
|
||||
import com.afollestad.nocknock.utilities.livedata.distinct
|
||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
|
||||
import com.afollestad.nocknock.viewcomponents.ext.isVisibleCondition
|
||||
import com.afollestad.nocknock.viewcomponents.ext.onScroll
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||
import com.afollestad.vvalidator.form
|
||||
import com.afollestad.vvalidator.form.Form
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalLayout
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.headersLayout
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.iconStatus
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.inputName
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.inputTags
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.inputUrl
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.loadingProgress
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.responseTimeoutInput
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.responseValidationMode
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.responseValidationSearchTerm
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.retryPolicyLayout
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.scriptInputLayout
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.scrollView
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.sslCertificateBrowse
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.sslCertificateInput
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.textLastCheckResult
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription
|
||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import kotlinx.android.synthetic.main.include_app_bar.app_toolbar as appToolbar
|
||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class ViewSiteActivity : DarkModeSwitchActivity() {
|
||||
companion object {
|
||||
private const val SELECT_CERT_FILE_RQ = 23
|
||||
}
|
||||
|
||||
internal val viewModel by viewModel<ViewSiteViewModel>()
|
||||
private lateinit var validationForm: Form
|
||||
|
||||
private val intentProvider by inject<IntentProvider>()
|
||||
private val statusUpdateReceiver by lazy {
|
||||
StatusUpdateIntentReceiver(application, intentProvider) {
|
||||
viewModel.setModel(it)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_viewsite)
|
||||
|
||||
// Populate view model with initial data
|
||||
val model = intent.getSerializableExtra(KEY_SITE) as Site
|
||||
viewModel.setModel(model)
|
||||
|
||||
setupUi()
|
||||
setupValidation()
|
||||
lifecycle.run {
|
||||
addObserver(viewModel)
|
||||
addObserver(statusUpdateReceiver)
|
||||
}
|
||||
|
||||
// Loading
|
||||
loadingProgress.observe(this, viewModel.onIsLoading())
|
||||
|
||||
// Status
|
||||
viewModel.status.observe(this, Observer {
|
||||
iconStatus.setStatus(it)
|
||||
invalidateMenuForStatus(it)
|
||||
})
|
||||
|
||||
// Name
|
||||
inputName.attachLiveData(this, viewModel.name)
|
||||
|
||||
// Tags
|
||||
inputTags.attachLiveData(this, viewModel.tags)
|
||||
|
||||
// Url
|
||||
inputUrl.attachLiveData(this, viewModel.url)
|
||||
viewModel.onUrlWarningVisibility()
|
||||
.toViewVisibility(this, textUrlWarning)
|
||||
|
||||
// Timeout
|
||||
responseTimeoutInput.attachLiveData(this, viewModel.timeout)
|
||||
|
||||
// Validation mode
|
||||
responseValidationMode.attachLiveData(
|
||||
lifecycleOwner = this,
|
||||
data = viewModel.validationMode,
|
||||
outTransformer = { ValidationMode.fromIndex(it) },
|
||||
inTransformer = { it.toIndex() }
|
||||
)
|
||||
viewModel.onValidationModeDescription()
|
||||
.toViewText(this, validationModeDescription)
|
||||
|
||||
// Validation search term
|
||||
responseValidationSearchTerm.attachLiveData(this, viewModel.validationSearchTerm)
|
||||
viewModel.onValidationSearchTermVisibility()
|
||||
.toViewVisibility(this, responseValidationSearchTerm)
|
||||
|
||||
// SSL certificate
|
||||
sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it }
|
||||
viewModel.certificateUri.distinct()
|
||||
.observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) })
|
||||
|
||||
// Headers
|
||||
headersLayout.attach(viewModel.headers)
|
||||
|
||||
// Last/next check
|
||||
viewModel.onLastCheckResultText()
|
||||
.toViewText(this, textLastCheckResult)
|
||||
viewModel.onNextCheckText()
|
||||
.toViewText(this, textNextCheck)
|
||||
}
|
||||
|
||||
private fun setupUi() {
|
||||
toolbarTitle.text = ""
|
||||
toolbar.run {
|
||||
setNavigationIcon(R.drawable.ic_action_close)
|
||||
setNavigationOnClickListener { finish() }
|
||||
inflateMenu(R.menu.menu_viewsite)
|
||||
|
||||
menu.findItem(R.id.refresh)
|
||||
.setActionView(R.layout.menu_item_refresh_icon)
|
||||
.apply {
|
||||
actionView.setOnClickListener { viewModel.checkNow() }
|
||||
}
|
||||
|
||||
setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.remove -> maybeRemoveSite()
|
||||
R.id.disableChecks -> maybeDisableChecks()
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
scrollView.onScroll {
|
||||
appToolbar.elevation = if (it > appToolbar.measuredHeight / 2) {
|
||||
appToolbar.dimenFloat(R.dimen.default_elevation)
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
|
||||
val validationOptionsAdapter = ArrayAdapter(
|
||||
this,
|
||||
R.layout.list_item_spinner,
|
||||
resources.getStringArray(R.array.response_validation_options)
|
||||
)
|
||||
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
||||
responseValidationMode.adapter = validationOptionsAdapter
|
||||
|
||||
// Disabled button
|
||||
viewModel.onDisableChecksVisibility()
|
||||
.observe(this, Observer {
|
||||
toolbar.menu.findItem(R.id.disableChecks)
|
||||
.isVisible = it
|
||||
})
|
||||
|
||||
// Done item text
|
||||
viewModel.onDoneButtonText()
|
||||
.observe(this, Observer {
|
||||
toolbar.menu.findItem(R.id.commit)
|
||||
.setTitle(it)
|
||||
})
|
||||
|
||||
// SSL certificate
|
||||
sslCertificateBrowse.setOnClickListener {
|
||||
val intent = Intent(ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
}
|
||||
startActivityForResult(intent, SELECT_CERT_FILE_RQ)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupValidation() {
|
||||
validationForm = form {
|
||||
input(inputName, name = "Name") {
|
||||
isNotEmpty().description(R.string.please_enter_name)
|
||||
}
|
||||
input(inputUrl, name = "URL") {
|
||||
isNotEmpty().description(R.string.please_enter_url)
|
||||
isUrl().description(R.string.please_enter_valid_url)
|
||||
}
|
||||
input(responseValidationSearchTerm, name = "Search term") {
|
||||
conditional(responseValidationSearchTerm.isVisibleCondition()) {
|
||||
isNotEmpty().description(R.string.please_enter_search_term)
|
||||
}
|
||||
}
|
||||
input(responseTimeoutInput, name = "Timeout", optional = true) {
|
||||
isNumber().greaterThan(0)
|
||||
.description(R.string.please_enter_networkTimeout)
|
||||
}
|
||||
input(sslCertificateInput, name = "Certificate Path", optional = true) {
|
||||
isUri().hasScheme("file", "content")
|
||||
.that { it.host != null }
|
||||
.description(R.string.please_enter_validCertUri)
|
||||
}
|
||||
submitWith(toolbar.menu, R.id.commit) {
|
||||
viewModel.commit { finish() }
|
||||
}
|
||||
}
|
||||
|
||||
// Validation script
|
||||
scriptInputLayout.attach(
|
||||
codeData = viewModel.validationScript,
|
||||
visibility = viewModel.onValidationScriptVisibility(),
|
||||
form = validationForm
|
||||
)
|
||||
|
||||
// Check interval
|
||||
checkIntervalLayout.attach(
|
||||
valueData = viewModel.checkIntervalValue,
|
||||
multiplierData = viewModel.checkIntervalUnit,
|
||||
form = validationForm
|
||||
)
|
||||
|
||||
// Retry Policy
|
||||
retryPolicyLayout.attach(
|
||||
timesData = viewModel.retryPolicyTimes,
|
||||
minutesData = viewModel.retryPolicyMinutes,
|
||||
form = validationForm
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {
|
||||
appToolbar.dimenFloat(R.dimen.default_elevation)
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
resultData: Intent?
|
||||
) {
|
||||
super.onActivityResult(requestCode, resultCode, resultData)
|
||||
if (requestCode == SELECT_CERT_FILE_RQ && resultCode == RESULT_OK) {
|
||||
sslCertificateInput.setText(resultData?.data?.toString() ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
if (intent != null && intent.hasExtra(KEY_SITE)) {
|
||||
val newModel = intent.getSerializableExtra(KEY_SITE) as Site
|
||||
viewModel.setModel(newModel)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.viewsite
|
||||
|
||||
import android.widget.ImageView
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.model.Status
|
||||
import com.afollestad.nocknock.data.model.isPending
|
||||
import com.afollestad.nocknock.toHtml
|
||||
import com.afollestad.nocknock.utilities.ext.animateRotation
|
||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||
|
||||
const val KEY_SITE = "site_model"
|
||||
|
||||
internal fun ViewSiteActivity.maybeRemoveSite() {
|
||||
val model = viewModel.site
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.remove_site)
|
||||
message(text = context.getString(R.string.remove_site_prompt, model.name).toHtml())
|
||||
positiveButton(R.string.remove) {
|
||||
viewModel.removeSite { finish() }
|
||||
}
|
||||
negativeButton(android.R.string.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ViewSiteActivity.maybeDisableChecks() {
|
||||
val model = viewModel.site
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.disable_automatic_checks)
|
||||
message(
|
||||
text = context.getString(R.string.disable_automatic_checks_prompt, model.name).toHtml()
|
||||
)
|
||||
positiveButton(R.string.disable) { viewModel.disableSite() }
|
||||
negativeButton(android.R.string.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ViewSiteActivity.invalidateMenuForStatus(status: Status) {
|
||||
val refreshIcon = toolbar.menu.findItem(R.id.refresh)
|
||||
.actionView as ImageView
|
||||
if (status.isPending()) {
|
||||
refreshIcon.animateRotation()
|
||||
} else {
|
||||
refreshIcon.run {
|
||||
animate().cancel()
|
||||
rotation = 0f
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,59 +0,0 @@
|
|||
package com.afollestad.nocknock.util;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import com.afollestad.nocknock.api.ServerModel;
|
||||
import com.afollestad.nocknock.services.CheckService;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
public class AlarmUtil {
|
||||
|
||||
private final static int BASE_RQC = 69;
|
||||
|
||||
public static PendingIntent getSiteIntent(Context context, ServerModel site) {
|
||||
return PendingIntent.getService(context,
|
||||
BASE_RQC + (int) site.id,
|
||||
new Intent(context, CheckService.class)
|
||||
.putExtra(CheckService.MODEL_ID, site.id),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
private static AlarmManager am(Context context) {
|
||||
return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
}
|
||||
|
||||
public static void cancelSiteChecks(Context context, ServerModel site) {
|
||||
PendingIntent pi = getSiteIntent(context, site);
|
||||
am(context).cancel(pi);
|
||||
}
|
||||
|
||||
public static void setSiteChecks(Context context, ServerModel site) {
|
||||
cancelSiteChecks(context, site);
|
||||
if (site.checkInterval <= 0) return;
|
||||
if (site.lastCheck <= 0)
|
||||
site.lastCheck = System.currentTimeMillis();
|
||||
final long nextCheck = site.lastCheck + site.checkInterval;
|
||||
final AlarmManager aMgr = am(context);
|
||||
final PendingIntent serviceIntent = getSiteIntent(context, site);
|
||||
aMgr.setRepeating(AlarmManager.RTC_WAKEUP, nextCheck, site.checkInterval, serviceIntent);
|
||||
final SimpleDateFormat df = new SimpleDateFormat("EEE MMM dd hh:mm:ssa z yyyy", Locale.getDefault());
|
||||
Log.d("AlarmUtil", String.format(Locale.getDefault(), "Set site check alarm for %s (%s), check interval: %d, next check: %s",
|
||||
site.name, site.url, site.checkInterval, df.format(new Date(nextCheck))));
|
||||
}
|
||||
|
||||
public static void setSiteChecks(Context context, ServerModel[] sites) {
|
||||
if (sites == null || sites.length == 0) return;
|
||||
for (ServerModel site : sites)
|
||||
setSiteChecks(context, site);
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
package com.afollestad.nocknock.util;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.mozilla.javascript.Context;
|
||||
import org.mozilla.javascript.EvaluatorException;
|
||||
import org.mozilla.javascript.Function;
|
||||
import org.mozilla.javascript.Scriptable;
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
public class JsUtil {
|
||||
|
||||
public static String exec(String code, String response) {
|
||||
try {
|
||||
final String func = String.format(
|
||||
"function validate(response) { " +
|
||||
"try { " +
|
||||
"%s " +
|
||||
"} catch(e) { " +
|
||||
"return e; " +
|
||||
"} " +
|
||||
"}", code.replace("\n", " "));
|
||||
|
||||
// Every Rhino VM begins with the enter()
|
||||
// This Context is not Android's Context
|
||||
Context rhino = Context.enter();
|
||||
|
||||
// Turn off optimization to make Rhino Android compatible
|
||||
rhino.setOptimizationLevel(-1);
|
||||
try {
|
||||
Scriptable scope = rhino.initStandardObjects();
|
||||
|
||||
// Note the forth argument is 1, which means the JavaScript source has
|
||||
// been compressed to only one line using something like YUI
|
||||
rhino.evaluateString(scope, func, "JavaScript", 1, null);
|
||||
|
||||
// Get the functionName defined in JavaScriptCode
|
||||
Function jsFunction = (Function) scope.get("validate", scope);
|
||||
|
||||
// Call the function with params
|
||||
Object jsResult = jsFunction.call(rhino, scope, scope, new Object[]{response});
|
||||
|
||||
// Parse the jsResult object to a String
|
||||
String result = Context.toString(jsResult);
|
||||
|
||||
boolean success = result != null && result.equals("true");
|
||||
String message = "The script returned a value other than true!";
|
||||
if (!success && result != null && !result.equals("false")) {
|
||||
if (result.equals("undefined")) {
|
||||
message = "The script did not return or throw anything!";
|
||||
} else {
|
||||
message = result;
|
||||
}
|
||||
}
|
||||
|
||||
Log.d("JsUtil", "Evaluated to " + message + " (" + success + "): " + code);
|
||||
return !success ? message : null;
|
||||
} finally {
|
||||
Context.exit();
|
||||
}
|
||||
} catch (EvaluatorException e) {
|
||||
return e.getMessage();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private JsUtil() {
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package com.afollestad.nocknock.util;
|
||||
|
||||
import android.graphics.Path;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
public final class MathUtil {
|
||||
|
||||
public static Path bezierCurve(FloatingActionButton fab, View rootView) {
|
||||
final int fabCenterX = (int) (fab.getX() + fab.getMeasuredWidth() / 2);
|
||||
final int fabCenterY = (int) (fab.getY() + fab.getMeasuredHeight() / 2);
|
||||
|
||||
final int endCenterX = (rootView.getMeasuredWidth() / 2) - (fab.getMeasuredWidth() / 2);
|
||||
final int endCenterY = (rootView.getMeasuredHeight() / 2) - (fab.getMeasuredHeight() / 2);
|
||||
|
||||
final int halfX = (fabCenterX - endCenterX) / 2;
|
||||
final int halfY = (fabCenterY - endCenterY) / 2;
|
||||
int mControlX = endCenterX + halfX;
|
||||
int mControlY = endCenterY + halfY;
|
||||
mControlY -= halfY;
|
||||
mControlX += halfX;
|
||||
|
||||
Path path = new Path();
|
||||
path.moveTo(fab.getX(), fab.getY());
|
||||
path.quadTo(mControlX, mControlY, endCenterX, endCenterY);
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package com.afollestad.nocknock.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
public class NetworkUtil {
|
||||
|
||||
public static boolean hasInternet(Context context) {
|
||||
final ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
final NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
|
||||
return activeNetwork != null &&
|
||||
activeNetwork.isConnectedOrConnecting();
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package com.afollestad.nocknock.util;
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
public class TimeUtil {
|
||||
|
||||
public final static long SECOND = 1000;
|
||||
public final static long MINUTE = SECOND * 60;
|
||||
public final static long HOUR = MINUTE * 60;
|
||||
public final static long DAY = HOUR * 24;
|
||||
public final static long WEEK = DAY * 7;
|
||||
public final static long MONTH = WEEK * 4;
|
||||
|
||||
public static String str(long duration) {
|
||||
if (duration <= 0) {
|
||||
return "";
|
||||
} else if (duration >= MONTH) {
|
||||
return (int) Math.ceil(((float) duration / (float) MONTH)) + "mo";
|
||||
} else if (duration >= WEEK) {
|
||||
return (int) Math.ceil(((float) duration / (float) WEEK)) + "w";
|
||||
} else if (duration >= DAY) {
|
||||
return (int) Math.ceil(((float) duration / (float) DAY)) + "d";
|
||||
} else if (duration >= HOUR) {
|
||||
return (int) Math.ceil(((float) duration / (float) HOUR)) + "h";
|
||||
} else if (duration >= MINUTE) {
|
||||
return (int) Math.ceil(((float) duration / (float) MINUTE)) + "m";
|
||||
} else {
|
||||
return "<1m";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
package com.afollestad.nocknock.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
|
||||
|
||||
private static final int[] ATTRS = new int[]{
|
||||
android.R.attr.listDivider
|
||||
};
|
||||
|
||||
public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
|
||||
|
||||
public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
|
||||
|
||||
private Drawable mDivider;
|
||||
|
||||
private int mOrientation;
|
||||
|
||||
public DividerItemDecoration(Context context, int orientation) {
|
||||
final TypedArray a = context.obtainStyledAttributes(ATTRS);
|
||||
mDivider = a.getDrawable(0);
|
||||
a.recycle();
|
||||
setOrientation(orientation);
|
||||
}
|
||||
|
||||
public void setOrientation(int orientation) {
|
||||
if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
|
||||
throw new IllegalArgumentException("invalid orientation");
|
||||
}
|
||||
mOrientation = orientation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas c, RecyclerView parent) {
|
||||
if (mOrientation == VERTICAL_LIST) {
|
||||
drawVertical(c, parent);
|
||||
} else {
|
||||
drawHorizontal(c, parent);
|
||||
}
|
||||
}
|
||||
|
||||
public void drawVertical(Canvas c, RecyclerView parent) {
|
||||
final int left = parent.getPaddingLeft();
|
||||
final int right = parent.getWidth() - parent.getPaddingRight();
|
||||
|
||||
final int childCount = parent.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = parent.getChildAt(i);
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
|
||||
.getLayoutParams();
|
||||
final int top = child.getBottom() + params.bottomMargin;
|
||||
final int bottom = top + mDivider.getIntrinsicHeight();
|
||||
mDivider.setBounds(left, top, right, bottom);
|
||||
mDivider.draw(c);
|
||||
}
|
||||
}
|
||||
|
||||
public void drawHorizontal(Canvas c, RecyclerView parent) {
|
||||
final int top = parent.getPaddingTop();
|
||||
final int bottom = parent.getHeight() - parent.getPaddingBottom();
|
||||
|
||||
final int childCount = parent.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = parent.getChildAt(i);
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
|
||||
.getLayoutParams();
|
||||
final int left = child.getRight() + params.rightMargin;
|
||||
final int right = left + mDivider.getIntrinsicHeight();
|
||||
mDivider.setBounds(left, top, right, bottom);
|
||||
mDivider.draw(c);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
|
||||
if (mOrientation == VERTICAL_LIST) {
|
||||
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
|
||||
} else {
|
||||
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package com.afollestad.nocknock.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.afollestad.nocknock.R;
|
||||
import com.afollestad.nocknock.api.ServerStatus;
|
||||
|
||||
/**
|
||||
* @author Aidan Follestad (afollestad)
|
||||
*/
|
||||
public class StatusImageView extends ImageView {
|
||||
|
||||
public StatusImageView(Context context) {
|
||||
super(context);
|
||||
setStatus(ServerStatus.OK);
|
||||
}
|
||||
|
||||
public StatusImageView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setStatus(ServerStatus.OK);
|
||||
}
|
||||
|
||||
public StatusImageView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
setStatus(ServerStatus.OK);
|
||||
}
|
||||
|
||||
public void setStatus(@ServerStatus.Enum int status) {
|
||||
switch (status) {
|
||||
case ServerStatus.CHECKING:
|
||||
case ServerStatus.WAITING:
|
||||
setImageResource(R.drawable.status_progress);
|
||||
setBackgroundResource(R.drawable.yellow_circle);
|
||||
break;
|
||||
case ServerStatus.ERROR:
|
||||
setImageResource(R.drawable.status_error);
|
||||
setBackgroundResource(R.drawable.red_circle);
|
||||
break;
|
||||
case ServerStatus.OK:
|
||||
setImageResource(R.drawable.status_ok);
|
||||
setBackgroundResource(R.drawable.green_circle);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +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
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>
|
Binary file not shown.
Before Width: | Height: | Size: 998 B |
Binary file not shown.
Before Width: | Height: | Size: 661 B |
Binary file not shown.
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.9 KiB |
13
app/src/main/res/drawable/checked_chip.xml
Normal file
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
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
5
app/src/main/res/drawable/checked_chip_selector.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/checked_chip" android:state_pressed="false"/>
|
||||
<item android:drawable="@drawable/checked_chip_pressed" android:state_pressed="true"/>
|
||||
</selector>
|
|
@ -1,4 +1,4 @@
|
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<size android:height="1dp" />
|
||||
<solid android:color="@color/dividerColor" />
|
||||
</shape>
|
||||
<size android:height="1dp"/>
|
||||
<solid android:color="?dividerColor"/>
|
||||
</shape>
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<solid android:color="@color/md_green" />
|
||||
|
||||
<stroke android:color="#424242" />
|
||||
|
||||
<size
|
||||
android:width="@dimen/list_circle_size"
|
||||
android:height="@dimen/list_circle_size" />
|
||||
</shape>
|
|
@ -3,7 +3,7 @@
|
|||
android:height="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
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" />
|
||||
<path
|
||||
android:fillColor="?iconColor"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
android:height="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
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>
|
||||
<path
|
||||
android:fillColor="?iconColor"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
android:height="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
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>
|
||||
<path
|
||||
android:fillColor="?iconColor"
|
||||
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
|
||||
</vector>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
android:height="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</vector>
|
||||
|
|
10
app/src/main/res/drawable/ic_check.xml
Normal file
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>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="84dp"
|
||||
android:height="84dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:pathData="M20,12l-1.41,-1.41L13,16.17V4h-2v12.17l-5.58,-5.59L4,12l8,8 8,-8z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<solid android:color="@color/md_red" />
|
||||
|
||||
<stroke android:color="#424242"/>
|
||||
|
||||
<size
|
||||
android:width="@dimen/list_circle_size"
|
||||
android:height="@dimen/list_circle_size" />
|
||||
</shape>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
|
||||
</vector>
|
13
app/src/main/res/drawable/unchecked_chip.xml
Normal file
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
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
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,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<solid android:color="@color/md_yellow" />
|
||||
|
||||
<stroke android:color="#424242" />
|
||||
|
||||
<size
|
||||
android:width="@dimen/list_circle_size"
|
||||
android:height="@dimen/list_circle_size" />
|
||||
</shape>
|
|
@ -1,242 +1,222 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/rootView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?colorPrimary"
|
||||
android:orientation="vertical">
|
||||
>
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:navigationIcon="@drawable/ic_action_close"
|
||||
app:title="@string/add_site"
|
||||
app:titleTextColor="?android:textColorPrimary" />
|
||||
<LinearLayout
|
||||
android:id="@+id/rootView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
>
|
||||
|
||||
<include layout="@layout/include_app_bar"/>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/content_inset_double"
|
||||
android:paddingLeft="@dimen/content_inset"
|
||||
android:paddingRight="@dimen/content_inset"
|
||||
android:paddingTop="@dimen/content_inset_half"
|
||||
>
|
||||
|
||||
<TextView
|
||||
android:layout_marginTop="0dp"
|
||||
android:text="@string/site_name"
|
||||
style="@style/InputForm.Header"
|
||||
/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/inputName"
|
||||
android:hint="@string/site_name_hint"
|
||||
android:inputType="textPersonName|textCapWords|textAutoCorrect"
|
||||
android:nextFocusDown="@+id/inputUrl"
|
||||
tools:ignore="Autofill"
|
||||
style="@style/InputForm.Field"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:text="@string/site_url"
|
||||
style="@style/InputForm.Header"
|
||||
/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/inputUrl"
|
||||
android:hint="@string/site_url_hint"
|
||||
android:inputType="textUri"
|
||||
android:nextFocusDown="@+id/inputTags"
|
||||
tools:ignore="Autofill"
|
||||
style="@style/InputForm.Field"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textUrlWarning"
|
||||
android:text="@string/warning_http_url"
|
||||
android:visibility="gone"
|
||||
style="@style/InputForm.FieldNote"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:text="@string/site_tags"
|
||||
style="@style/InputForm.Header"
|
||||
/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/inputTags"
|
||||
android:digits=",.?-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ "
|
||||
android:hint="@string/site_tags_hint"
|
||||
android:inputType="text|textCapWords"
|
||||
android:nextFocusDown="@+id/inputUrl"
|
||||
tools:ignore="Autofill"
|
||||
style="@style/InputForm.Field"
|
||||
/>
|
||||
|
||||
<include layout="@layout/include_divider"/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.interval.ValidationIntervalLayout
|
||||
android:id="@+id/checkIntervalLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/responseValidationLabel"
|
||||
android:text="@string/response_validation_mode"
|
||||
style="@style/InputForm.Header"
|
||||
/>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/responseValidationMode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/responseValidationSearchTerm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/content_inset_less"
|
||||
android:layout_marginLeft="-4dp"
|
||||
android:layout_marginRight="-4dp"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:hint="@string/search_term"
|
||||
android:visibility="gone"
|
||||
tools:ignore="Autofill,TextFields"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.js.JavaScriptInputLayout
|
||||
android:id="@+id/scriptInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/content_inset"
|
||||
android:layout_marginTop="@dimen/content_inset_half"
|
||||
android:background="?scriptLayoutBackground"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/validationModeDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/content_inset_half"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:text="@string/validation_mode_status_desc"
|
||||
style="@style/NockText.Body.Light"
|
||||
/>
|
||||
|
||||
<include layout="@layout/include_divider"/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.retrypolicy.RetryPolicyLayout
|
||||
android:id="@+id/retryPolicyLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset_more"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:text="@string/response_timeout"
|
||||
style="@style/InputForm.Header"
|
||||
/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/responseTimeoutInput"
|
||||
android:hint="@string/response_timeout_default"
|
||||
android:inputType="number"
|
||||
android:maxLength="8"
|
||||
tools:ignore="Autofill"
|
||||
style="@style/InputForm.Field"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:text="@string/ssl_certificate"
|
||||
style="@style/NockText.SectionHeader"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/content_inset"
|
||||
android:paddingLeft="@dimen/content_inset"
|
||||
android:paddingRight="@dimen/content_inset">
|
||||
android:orientation="horizontal"
|
||||
>
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/nameTiLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="-4dp"
|
||||
android:layout_marginRight="-4dp"
|
||||
android:layout_marginTop="@dimen/content_inset">
|
||||
<EditText
|
||||
android:id="@+id/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"
|
||||
/>
|
||||
|
||||
<android.support.design.widget.TextInputEditText
|
||||
android:id="@+id/inputName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:hint="@string/site_name"
|
||||
android:inputType="textPersonName|textCapWords|textAutoCorrect"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textColorHint="?android:textColorSecondary"
|
||||
android:textSize="@dimen/body_font_size" />
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/urlTiLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="-4dp"
|
||||
android:layout_marginRight="-4dp"
|
||||
android:layout_marginTop="@dimen/content_inset_half">
|
||||
|
||||
<android.support.design.widget.TextInputEditText
|
||||
android:id="@+id/inputUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:hint="@string/site_url"
|
||||
android:inputType="textUri"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textColorHint="?android:textColorSecondary"
|
||||
android:textSize="@dimen/body_font_size" />
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textUrlWarning"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/list_text_spacing"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="@dimen/caption_font_size"
|
||||
android:visibility="gone"
|
||||
tools:text="Warning: this app checks for server availability with HTTP requests. It's recommended that you use an HTTP URL." />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:background="@color/dividerColorDark" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/checkIntervalLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/check_interval"
|
||||
android:textColor="?colorAccent"
|
||||
android:textSize="@dimen/caption_font_size" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="2">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/checkIntervalInput"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:layout_marginEnd="@dimen/content_inset_half"
|
||||
android:layout_marginStart="-4dp"
|
||||
android:layout_weight="1"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:hint="0"
|
||||
android:inputType="number"
|
||||
android:textSize="@dimen/body_font_size"
|
||||
tools:ignore="HardcodedText,LabelFor" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/checkIntervalSpinner"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_marginEnd="-4dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:background="@color/dividerColorDark" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/responseValidation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/response_validation_mode"
|
||||
android:textColor="?colorAccent"
|
||||
android:textSize="@dimen/caption_font_size" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/responseValidationMode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/responseValidationSearchTerm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/content_inset_less"
|
||||
android:layout_marginLeft="-4dp"
|
||||
android:layout_marginRight="-4dp"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:hint="@string/search_term"
|
||||
android:textSize="@dimen/body_font_size"
|
||||
android:visibility="gone" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/responseValidationScript"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/content_inset"
|
||||
android:layout_marginTop="@dimen/content_inset_half"
|
||||
android:background="@color/colorPrimaryDark"
|
||||
android:elevation="@dimen/fab_elevation"
|
||||
android:padding="@dimen/content_inset_half"
|
||||
android:scrollbars="none">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="serif-monospace"
|
||||
android:lineSpacingMultiplier="1.4"
|
||||
android:singleLine="true"
|
||||
android:text="@string/function_declaration"
|
||||
android:textSize="@dimen/code_font_size" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/responseValidationScriptInput"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@null"
|
||||
android:fontFamily="serif-monospace"
|
||||
android:gravity="top"
|
||||
android:hint="@string/default_js"
|
||||
android:inputType="textMultiLine"
|
||||
android:lineSpacingMultiplier="1.4"
|
||||
android:paddingBottom="@dimen/content_inset_less"
|
||||
android:paddingEnd="@dimen/content_inset_more"
|
||||
android:paddingStart="@dimen/content_inset_more"
|
||||
android:paddingTop="@dimen/content_inset_less"
|
||||
android:scrollHorizontally="true"
|
||||
android:textSize="@dimen/code_font_size"
|
||||
tools:ignore="LabelFor,RtlSymmetry" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="serif-monospace"
|
||||
android:text="@string/function_end"
|
||||
android:textSize="@dimen/code_font_size" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/validationModeDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/content_inset_half"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:text="@string/validation_mode_status_desc"
|
||||
android:textSize="@dimen/body_font_size" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/doneBtn"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="-4dp"
|
||||
android:layout_marginRight="-4dp"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:text="@string/done"
|
||||
android:textColor="#fff" />
|
||||
<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>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.LoadingIndicatorFrame
|
||||
android:id="@+id/loadingProgress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
/>
|
||||
|
||||
</FrameLayout>
|
||||
|
|
|
@ -1,105 +1,71 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/rootView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.MainActivity">
|
||||
tools:context=".ui.main.MainActivity"
|
||||
>
|
||||
|
||||
<android.support.v4.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
>
|
||||
|
||||
<include layout="@layout/include_app_bar"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/tags_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
</android.support.v4.widget.SwipeRefreshLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginBottom="@dimen/content_inset"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:gravity="center"
|
||||
android:text="@string/no_sites_added"
|
||||
android:textSize="@dimen/title_font_size"
|
||||
android:textStyle="italic" />
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:src="@drawable/ic_add"
|
||||
app:backgroundTint="?colorAccent"
|
||||
app:elevation="@dimen/fab_elevation"
|
||||
app:fabSize="normal"
|
||||
app:pressedTranslationZ="@dimen/fab_elevation_pressed"
|
||||
app:rippleColor="#40ffffff"
|
||||
app:useCompatPadding="true" />
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="@dimen/content_inset_half"
|
||||
android:paddingEnd="@dimen/content_inset"
|
||||
android:paddingStart="@dimen/content_inset"
|
||||
android:paddingTop="@dimen/content_inset_half"
|
||||
android:scrollbars="none"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/swipeRefreshTutorial"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#BF000000"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/content_inset"
|
||||
android:visibility="gone">
|
||||
android:scrollbars="vertical"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="2">
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_down_arrow"
|
||||
android:tint="#fff"
|
||||
tools:ignore="ContentDescription" />
|
||||
<include layout="@layout/include_empty_view"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_down_arrow"
|
||||
android:tint="#fff"
|
||||
tools:ignore="ContentDescription" />
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:layout_marginBottom="@dimen/content_inset"
|
||||
android:layout_marginEnd="@dimen/content_inset_more"
|
||||
android:minHeight="64dp"
|
||||
android:paddingBottom="@dimen/content_inset_half"
|
||||
android:paddingEnd="@dimen/content_inset"
|
||||
android:paddingStart="@dimen/content_inset"
|
||||
android:paddingTop="@dimen/content_inset_half"
|
||||
android:text="@string/add_site"
|
||||
app:cornerRadius="32dp"
|
||||
app:icon="@drawable/ic_add"
|
||||
app:iconTint="#fff"
|
||||
style="@style/Widget.MaterialComponents.Button.Icon"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
<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"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="@dimen/content_inset_more"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:gravity="center"
|
||||
android:lineSpacingMultiplier="1.6"
|
||||
android:text="@string/swipe_refresh_hint"
|
||||
android:textColor="#fff"
|
||||
android:textSize="@dimen/medium_text_size" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/understoodBtn"
|
||||
android:layout_width="@dimen/tutorial_button_width"
|
||||
android:layout_height="@dimen/button_height"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="@dimen/content_inset_double"
|
||||
android:text="@string/understood"
|
||||
android:theme="@style/AccentButton" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
|
|
|
@ -1,295 +1,309 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/rootView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?colorPrimary"
|
||||
android:orientation="vertical">
|
||||
>
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:navigationIcon="@drawable/ic_action_close"
|
||||
app:title="@string/view_site"
|
||||
app:titleTextColor="?android:textColorPrimary" />
|
||||
<LinearLayout
|
||||
android:id="@+id/rootView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?colorPrimary"
|
||||
android:orientation="vertical"
|
||||
>
|
||||
|
||||
<include layout="@layout/include_app_bar"/>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/content_inset_double"
|
||||
android:paddingLeft="@dimen/content_inset"
|
||||
android:paddingRight="@dimen/content_inset"
|
||||
android:paddingTop="@dimen/content_inset_less"
|
||||
>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/inputName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@null"
|
||||
android:hint="@string/site_name"
|
||||
android:inputType="textPersonName|textCapWords|textAutoCorrect"
|
||||
android:nextFocusDown="@+id/inputUrl"
|
||||
android:singleLine="true"
|
||||
android:transitionName="site_name"
|
||||
tools:ignore="Autofill,UnusedAttribute"
|
||||
style="@style/NockText.Header"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/content_inset"
|
||||
android:paddingLeft="@dimen/content_inset"
|
||||
android:paddingRight="@dimen/content_inset"
|
||||
android:paddingTop="@dimen/content_inset_half">
|
||||
android:layout_marginTop="@dimen/content_inset_quarter"
|
||||
android:orientation="horizontal"
|
||||
>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<com.afollestad.nocknock.viewcomponents.StatusImageView
|
||||
android:id="@+id/iconStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="@dimen/content_inset"
|
||||
android:scaleType="centerInside"
|
||||
android:transitionName="status_image_view"
|
||||
tools:ignore="ContentDescription,UnusedAttribute"
|
||||
/>
|
||||
|
||||
<com.afollestad.nocknock.views.StatusImageView
|
||||
android:id="@+id/iconStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="@dimen/content_inset"
|
||||
android:scaleType="centerInside"
|
||||
android:transitionName="status_image_view"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/inputName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:hint="@string/site_name"
|
||||
android:inputType="textPersonName|textCapWords|textAutoCorrect"
|
||||
android:singleLine="true"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textColorHint="?android:textColorSecondary"
|
||||
android:textSize="@dimen/body_font_size"
|
||||
android:transitionName="site_name" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/inputUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:hint="@string/site_url"
|
||||
android:inputType="textUri"
|
||||
android:singleLine="true"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textColorHint="?android:textColorSecondary"
|
||||
android:textSize="@dimen/body_font_size"
|
||||
android:transitionName="site_url" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textUrlWarning"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="@dimen/list_text_spacing"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="@dimen/caption_font_size"
|
||||
android:visibility="gone"
|
||||
tools:text="Warning: this app checks for server availability with HTTP requests. It's recommended that you use an HTTP URL." />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="@dimen/content_inset_less"
|
||||
android:background="@color/dividerColorDark" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/checkIntervalLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/check_interval"
|
||||
android:textColor="?colorAccent"
|
||||
android:textSize="@dimen/caption_font_size" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="2">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/checkIntervalInput"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:layout_marginEnd="@dimen/content_inset_half"
|
||||
android:layout_marginStart="-4dp"
|
||||
android:layout_weight="1"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:hint="0"
|
||||
android:inputType="number"
|
||||
android:textSize="@dimen/body_font_size"
|
||||
tools:ignore="HardcodedText,LabelFor" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/checkIntervalSpinner"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_marginEnd="-4dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="@dimen/content_inset_less"
|
||||
android:background="@color/dividerColorDark" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/responseValidation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/response_validation_mode"
|
||||
android:textColor="?colorAccent"
|
||||
android:textSize="@dimen/caption_font_size" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/responseValidationMode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/responseValidationSearchTerm"
|
||||
android:id="@+id/inputUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/content_inset_less"
|
||||
android:layout_marginLeft="-4dp"
|
||||
android:layout_marginRight="-4dp"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:hint="@string/search_term"
|
||||
android:textSize="@dimen/body_font_size"
|
||||
android:visibility="gone" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/responseValidationScript"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/content_inset"
|
||||
android:layout_marginTop="@dimen/content_inset_half"
|
||||
android:background="@color/colorPrimaryDark"
|
||||
android:elevation="@dimen/fab_elevation"
|
||||
android:padding="@dimen/content_inset_half"
|
||||
android:scrollbars="none">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="serif-monospace"
|
||||
android:lineSpacingMultiplier="1.4"
|
||||
android:singleLine="true"
|
||||
android:text="@string/function_declaration"
|
||||
android:textSize="@dimen/code_font_size" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/responseValidationScriptInput"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@null"
|
||||
android:fontFamily="serif-monospace"
|
||||
android:gravity="top"
|
||||
android:hint="@string/default_js"
|
||||
android:inputType="textMultiLine"
|
||||
android:lineSpacingMultiplier="1.4"
|
||||
android:paddingBottom="@dimen/content_inset_less"
|
||||
android:paddingEnd="@dimen/content_inset_more"
|
||||
android:paddingStart="@dimen/content_inset_more"
|
||||
android:paddingTop="@dimen/content_inset_less"
|
||||
android:scrollHorizontally="true"
|
||||
android:textSize="@dimen/code_font_size"
|
||||
tools:ignore="LabelFor,RtlSymmetry" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="serif-monospace"
|
||||
android:text="@string/function_end"
|
||||
android:textSize="@dimen/code_font_size" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/validationModeDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/content_inset_half"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:text="@string/validation_mode_status_desc"
|
||||
android:textSize="@dimen/body_font_size" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:background="@color/dividerColorDark" />
|
||||
android:hint="@string/site_url"
|
||||
android:inputType="textUri"
|
||||
android:nextFocusDown="@+id/inputTags"
|
||||
android:singleLine="true"
|
||||
android:transitionName="site_url"
|
||||
tools:ignore="Autofill,UnusedAttribute"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textUrlWarning"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:text="@string/last_check_result"
|
||||
android:textColor="?colorAccent"
|
||||
android:textSize="@dimen/caption_font_size" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textLastCheckResult"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="@dimen/list_text_spacing"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="@dimen/medium_text_size"
|
||||
tools:text="Everything checks out!" />
|
||||
android:visibility="gone"
|
||||
tools:text="Warning: this app checks for server availability with HTTP requests. It's recommended that you use an HTTP URL."
|
||||
style="@style/NockText.Footnote"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:text="@string/next_check"
|
||||
android:textColor="?colorAccent"
|
||||
android:textSize="@dimen/caption_font_size" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textNextCheck"
|
||||
<EditText
|
||||
android:id="@+id/inputTags"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/list_text_spacing"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="@dimen/medium_text_size"
|
||||
tools:text="In 2 hours" />
|
||||
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"
|
||||
/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/doneBtn"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="-4dp"
|
||||
android:layout_marginRight="-4dp"
|
||||
android:layout_marginTop="@dimen/content_inset_more"
|
||||
android:text="@string/save"
|
||||
android:textColor="#fff" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include
|
||||
layout="@layout/include_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="@dimen/content_inset_less"
|
||||
/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.interval.ValidationIntervalLayout
|
||||
android:id="@+id/checkIntervalLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/responseValidationLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:text="@string/response_validation_mode"
|
||||
style="@style/NockText.SectionHeader"
|
||||
/>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/responseValidationMode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/responseValidationSearchTerm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/content_inset_less"
|
||||
android:layout_marginLeft="-4dp"
|
||||
android:layout_marginRight="-4dp"
|
||||
android:layout_marginTop="-4dp"
|
||||
android:hint="@string/search_term"
|
||||
android:visibility="gone"
|
||||
tools:ignore="Autofill,TextFields"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.js.JavaScriptInputLayout
|
||||
android:id="@+id/scriptInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/content_inset"
|
||||
android:layout_marginTop="@dimen/content_inset_half"
|
||||
android:background="?scriptLayoutBackground"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/validationModeDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/content_inset_half"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:text="@string/validation_mode_status_desc"
|
||||
style="@style/NockText.Body.Light"
|
||||
/>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="@dimen/content_inset_less"
|
||||
android:background="?dividerColor"
|
||||
/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.retrypolicy.RetryPolicyLayout
|
||||
android:id="@+id/retryPolicyLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:text="@string/response_timeout"
|
||||
style="@style/NockText.SectionHeader"
|
||||
/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/responseTimeoutInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="-4dp"
|
||||
android:layout_marginStart="-4dp"
|
||||
android:layout_marginTop="@dimen/content_inset_quarter"
|
||||
android:hint="@string/response_timeout_default"
|
||||
android:inputType="number"
|
||||
android:maxLength="8"
|
||||
tools:ignore="Autofill,HardcodedText,LabelFor"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:text="@string/ssl_certificate"
|
||||
style="@style/NockText.SectionHeader"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/sslCertificateInput"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:layout_marginStart="-4dp"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/ssl_certificate_automatic"
|
||||
android:inputType="textUri"
|
||||
tools:ignore="Autofill,HardcodedText,LabelFor"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/sslCertificateBrowse"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:text="@string/ssl_certificate_browse"
|
||||
style="@style/AccentTextButton"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include layout="@layout/include_divider"/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.headers.HeaderStackLayout
|
||||
android:id="@+id/headersLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset_half"
|
||||
/>
|
||||
|
||||
|
||||
<include layout="@layout/include_divider"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:text="@string/last_check_result"
|
||||
style="@style/NockText.SectionHeader"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textLastCheckResult"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/list_text_spacing"
|
||||
tools:text="Everything checks out!"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:text="@string/next_check"
|
||||
style="@style/NockText.SectionHeader"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textNextCheck"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/list_text_spacing"
|
||||
tools:text="In 2 hours"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.LoadingIndicatorFrame
|
||||
android:id="@+id/loadingProgress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
/>
|
||||
|
||||
</FrameLayout>
|
||||
|
|
30
app/src/main/res/layout/include_app_bar.xml
Normal file
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>
|
8
app/src/main/res/layout/include_divider.xml
Normal file
8
app/src/main/res/layout/include_divider.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<View
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:background="?dividerColor"
|
||||
/>
|
14
app/src/main/res/layout/include_empty_view.xml
Normal file
14
app/src/main/res/layout/include_empty_view.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/emptyText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginBottom="@dimen/content_inset"
|
||||
android:fontFamily="@font/lato_light"
|
||||
android:gravity="center"
|
||||
android:text="@string/no_sites_added"
|
||||
android:textSize="@dimen/empty_text_size"
|
||||
android:textStyle="italic"
|
||||
/>
|
|
@ -1,87 +1,86 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:orientation="horizontal"
|
||||
android:paddingBottom="@dimen/content_inset_less"
|
||||
android:paddingBottom="@dimen/content_inset"
|
||||
android:paddingLeft="@dimen/content_inset"
|
||||
android:paddingRight="@dimen/content_inset"
|
||||
android:paddingTop="@dimen/content_inset_less">
|
||||
android:paddingTop="@dimen/content_inset"
|
||||
>
|
||||
|
||||
<com.afollestad.nocknock.views.StatusImageView
|
||||
android:id="@+id/iconStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="@dimen/content_inset"
|
||||
android:scaleType="centerInside"
|
||||
android:transitionName="status_image_view"
|
||||
tools:ignore="ContentDescription" />
|
||||
<com.afollestad.nocknock.viewcomponents.StatusImageView
|
||||
android:id="@+id/iconStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="@dimen/content_inset"
|
||||
android:scaleType="centerInside"
|
||||
android:transitionName="status_image_view"
|
||||
tools:ignore="ContentDescription"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:orientation="horizontal"
|
||||
>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:id="@+id/textName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginEnd="@dimen/content_inset_half"
|
||||
android:layout_toStartOf="@+id/textInterval"
|
||||
android:singleLine="true"
|
||||
android:transitionName="site_name"
|
||||
tools:text="Website Name"
|
||||
style="@style/NockText.Title"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginEnd="@dimen/content_inset_half"
|
||||
android:layout_toStartOf="@+id/textInterval"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:singleLine="true"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="@dimen/title_font_size"
|
||||
android:transitionName="site_name"
|
||||
tools:text="Website Name" />
|
||||
<TextView
|
||||
android:id="@+id/textInterval"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
tools:text="1h"
|
||||
style="@style/NockText.Caption"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textInterval"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:singleLine="true"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="@dimen/caption_font_size"
|
||||
tools:text="1h" />
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
<TextView
|
||||
android:id="@+id/textUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/list_text_spacing"
|
||||
android:singleLine="true"
|
||||
android:transitionName="site_url"
|
||||
tools:text="https://yourwebsitehere.com"
|
||||
style="@style/NockText.Body.Light"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/list_text_spacing"
|
||||
android:fontFamily="sans-serif"
|
||||
android:singleLine="true"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="@dimen/body_font_size"
|
||||
android:transitionName="site_url"
|
||||
tools:text="https://yourwebsitehere.com" />
|
||||
<TextView
|
||||
android:id="@+id/textStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/list_text_spacing"
|
||||
android:singleLine="true"
|
||||
tools:text="Everything checks out!"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textStatus"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/list_text_spacing"
|
||||
android:fontFamily="sans-serif"
|
||||
android:singleLine="true"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="@dimen/body_font_size"
|
||||
tools:text="Everything checks out!" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/button_height"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:gravity="center_vertical|start"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="@dimen/body_font_size" />
|
15
app/src/main/res/layout/list_item_tag.xml
Normal file
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/layout/menu_item_refresh_icon.xml
Normal file
9
app/src/main/res/layout/menu_item_refresh_icon.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ImageView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/refresh_status"
|
||||
android:src="@drawable/ic_action_refresh"
|
||||
style="@android:style/Widget.ActionButton"
|
||||
/>
|
9
app/src/main/res/menu/menu_addsite.xml
Normal file
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>
|
|
@ -1,8 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/about"
|
||||
android:title="@string/about" />
|
||||
|
||||
</menu>
|
||||
<item
|
||||
android:id="@+id/about"
|
||||
android:title="@string/about"/>
|
||||
<item
|
||||
android:id="@+id/dark_mode"
|
||||
android:checkable="true"
|
||||
android:title="@string/dark_mode"/>
|
||||
</menu>
|
||||
|
|
|
@ -1,17 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/refresh"
|
||||
android:icon="@drawable/ic_action_refresh"
|
||||
android:title="@string/refresh_status"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/remove"
|
||||
android:icon="@drawable/ic_action_delete"
|
||||
android:title="@string/remove_site"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
</menu>
|
||||
<item
|
||||
android:id="@+id/commit"
|
||||
android:icon="@drawable/ic_check"
|
||||
android:title="@string/save_changes"
|
||||
app:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/refresh"
|
||||
android:icon="@drawable/ic_action_refresh"
|
||||
android:title="@string/refresh_status"
|
||||
app:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/remove"
|
||||
android:icon="@drawable/ic_action_delete"
|
||||
android:title="@string/remove_site"
|
||||
app:showAsAction="ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/disableChecks"
|
||||
android:title="@string/disable_automatic_checks"
|
||||
/>
|
||||
</menu>
|
||||
|
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
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
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>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue