Update all deps, re-write everything in Kotlin, use Dagger, etc.
1
.idea/.name
generated
|
@ -1 +0,0 @@
|
|||
nock-nock
|
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
|
@ -1,3 +0,0 @@
|
|||
<component name="CopyrightManager">
|
||||
<settings default="" />
|
||||
</component>
|
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>
|
9
.idea/inspectionProfiles/Project_Default.xml
generated
|
@ -1,9 +0,0 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="LoggerInitializedWithForeignClass" enabled="false" level="WARNING" enabled_by_default="false">
|
||||
<option name="loggerClassName" value="org.apache.log4j.Logger,org.slf4j.LoggerFactory,org.apache.commons.logging.LogFactory,java.util.logging.Logger" />
|
||||
<option name="loggerFactoryMethodName" value="getLogger,getLogger,getLog,getLogger" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
7
.idea/inspectionProfiles/profiles_settings.xml
generated
|
@ -1,7 +0,0 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="PROJECT_PROFILE" value="Project Default" />
|
||||
<option name="USE_PROJECT_PROFILE" value="true" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
72
.idea/markdown-navigator.xml
generated
|
@ -1,72 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownProjectSettings">
|
||||
<PreviewSettings splitEditorLayout="SPLIT" splitEditorPreview="PREVIEW" useGrayscaleRendering="false" zoomFactor="1.0" maxImageWidth="0" showGitHubPageIfSynced="false" allowBrowsingInPreview="false" synchronizePreviewPosition="true" highlightPreviewType="NONE" highlightFadeOut="5" highlightOnTyping="true" synchronizeSourcePosition="true" verticallyAlignSourceAndPreviewSyncPosition="true" showSearchHighlightsInPreview="false" showSelectionInPreview="true">
|
||||
<PanelProvider>
|
||||
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.panel" providerName="Default - Swing" />
|
||||
</PanelProvider>
|
||||
</PreviewSettings>
|
||||
<ParserSettings gitHubSyntaxChange="false">
|
||||
<PegdownExtensions>
|
||||
<option name="ABBREVIATIONS" value="false" />
|
||||
<option name="ANCHORLINKS" value="true" />
|
||||
<option name="ASIDE" value="false" />
|
||||
<option name="ATXHEADERSPACE" value="true" />
|
||||
<option name="AUTOLINKS" value="true" />
|
||||
<option name="DEFINITIONS" value="false" />
|
||||
<option name="DEFINITION_BREAK_DOUBLE_BLANK_LINE" value="false" />
|
||||
<option name="FENCED_CODE_BLOCKS" value="true" />
|
||||
<option name="FOOTNOTES" value="false" />
|
||||
<option name="HARDWRAPS" value="false" />
|
||||
<option name="HTML_DEEP_PARSER" value="false" />
|
||||
<option name="INSERTED" value="false" />
|
||||
<option name="QUOTES" value="false" />
|
||||
<option name="RELAXEDHRULES" value="true" />
|
||||
<option name="SMARTS" value="false" />
|
||||
<option name="STRIKETHROUGH" value="true" />
|
||||
<option name="SUBSCRIPT" value="false" />
|
||||
<option name="SUPERSCRIPT" value="false" />
|
||||
<option name="SUPPRESS_HTML_BLOCKS" value="false" />
|
||||
<option name="SUPPRESS_INLINE_HTML" value="false" />
|
||||
<option name="TABLES" value="true" />
|
||||
<option name="TASKLISTITEMS" value="true" />
|
||||
<option name="TOC" value="false" />
|
||||
<option name="WIKILINKS" value="true" />
|
||||
</PegdownExtensions>
|
||||
<ParserOptions>
|
||||
<option name="COMMONMARK_LISTS" value="true" />
|
||||
<option name="DUMMY" value="false" />
|
||||
<option name="EMOJI_SHORTCUTS" value="true" />
|
||||
<option name="FLEXMARK_FRONT_MATTER" value="false" />
|
||||
<option name="GFM_LOOSE_BLANK_LINE_AFTER_ITEM_PARA" value="false" />
|
||||
<option name="GFM_TABLE_RENDERING" value="true" />
|
||||
<option name="GITBOOK_URL_ENCODING" value="false" />
|
||||
<option name="GITHUB_EMOJI_URL" value="false" />
|
||||
<option name="GITHUB_LISTS" value="false" />
|
||||
<option name="GITHUB_WIKI_LINKS" value="true" />
|
||||
<option name="JEKYLL_FRONT_MATTER" value="false" />
|
||||
<option name="SIM_TOC_BLANK_LINE_SPACER" value="true" />
|
||||
</ParserOptions>
|
||||
</ParserSettings>
|
||||
<HtmlSettings headerTopEnabled="false" headerBottomEnabled="false" bodyTopEnabled="false" bodyBottomEnabled="false" embedUrlContent="false" addPageHeader="true">
|
||||
<GeneratorProvider>
|
||||
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.generator" providerName="Default Swing HTML Generator" />
|
||||
</GeneratorProvider>
|
||||
<headerTop />
|
||||
<headerBottom />
|
||||
<bodyTop />
|
||||
<bodyBottom />
|
||||
</HtmlSettings>
|
||||
<CssSettings previewScheme="UI_SCHEME" cssUri="" isCssUriEnabled="false" isCssTextEnabled="false" isDynamicPageWidth="true">
|
||||
<StylesheetProvider>
|
||||
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.css" providerName="Default Swing Stylesheet" />
|
||||
</StylesheetProvider>
|
||||
<ScriptProviders />
|
||||
<cssText />
|
||||
</CssSettings>
|
||||
<HtmlExportSettings updateOnSave="false" parentDir="$ProjectFileDir$" targetDir="$ProjectFileDir$" cssDir="" scriptDir="" plainHtml="false" imageDir="" copyLinkedImages="false" imageUniquifyType="0" targetExt="" useTargetExt="false" noCssNoScripts="false" linkToExportedHtml="true" exportOnSettingsChange="true" regenerateOnProjectOpen="false" />
|
||||
<LinkMapSettings>
|
||||
<textMaps />
|
||||
</LinkMapSettings>
|
||||
</component>
|
||||
</project>
|
3
.idea/markdown-navigator/profiles_settings.xml
generated
|
@ -1,3 +0,0 @@
|
|||
<component name="MarkdownNavigator.ProfileManager">
|
||||
<settings default="" pdf-export="" />
|
||||
</component>
|
30
.idea/misc.xml
generated
|
@ -1,46 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EntryPointsManager">
|
||||
<entry_points version="2.0" />
|
||||
</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="7">
|
||||
<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" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myNotNulls">
|
||||
<value>
|
||||
<list size="4">
|
||||
<list size="6">
|
||||
<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" />
|
||||
</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_7" 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>
|
||||
|
|
9
.idea/modules.xml
generated
|
@ -2,11 +2,12 @@
|
|||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/NockNock.iml" filepath="$PROJECT_DIR$/NockNock.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/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$/nock-nock.iml" filepath="$PROJECT_DIR$/nock-nock.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/notifications/notifications.iml" filepath="$PROJECT_DIR$/notifications/notifications.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/utilities/utilities.iml" filepath="$PROJECT_DIR$/utilities/utilities.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
|
|
|
@ -4,8 +4,8 @@ android:
|
|||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-27.0.2
|
||||
- android-27
|
||||
- build-tools-28.0.3
|
||||
- android-28
|
||||
- extra-android-support
|
||||
- extra-android-m2repository
|
||||
- extra-google-m2repository
|
||||
|
@ -19,4 +19,4 @@ android:
|
|||
#- sys-img-x86-android-17
|
||||
|
||||
licenses:
|
||||
- '.+'
|
||||
- '.+'
|
||||
|
|
|
@ -1,40 +1,38 @@
|
|||
apply from: '../dependencies.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
buildToolsVersion versions.buildTools
|
||||
compileSdkVersion versions.compileSdk
|
||||
buildToolsVersion versions.buildTools
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.afollestad.nocknock"
|
||||
minSdkVersion versions.minSdk
|
||||
targetSdkVersion versions.compileSdk
|
||||
versionCode versions.publishVersionCode
|
||||
versionName versions.publishVersion
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
defaultConfig {
|
||||
applicationId "com.afollestad.nocknock"
|
||||
minSdkVersion versions.minSdk
|
||||
targetSdkVersion versions.compileSdk
|
||||
versionCode versions.publishVersionCode
|
||||
versionName versions.publishVersion
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile 'com.android.support:appcompat-v7:' + versions.supportLib
|
||||
compile 'com.android.support:design:' + versions.supportLib
|
||||
compile 'com.afollestad.material-dialogs:core:' + versions.materialDialogs
|
||||
compile 'com.afollestad.material-dialogs:commons:' + versions.materialDialogs
|
||||
compile 'com.afollestad:bridge:' + versions.bridge
|
||||
compile 'com.afollestad:inquiry:' + versions.inquiry
|
||||
compile files('libs/rhino-1.7.7.1.jar')
|
||||
}
|
||||
implementation project(':data')
|
||||
implementation project(':utilities')
|
||||
implementation project(':engine')
|
||||
implementation project(':notifications')
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:' + versions.androidx
|
||||
implementation 'androidx.recyclerview:recyclerview:' + versions.androidx
|
||||
implementation 'com.google.android.material:material:' + versions.androidx
|
||||
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin
|
||||
|
||||
implementation 'com.google.dagger:dagger:' + versions.dagger
|
||||
kapt 'com.google.dagger:dagger-compiler:' + versions.dagger
|
||||
|
||||
implementation 'com.afollestad.material-dialogs:core:' + versions.materialDialogs
|
||||
}
|
||||
|
||||
apply from: '../spotless.gradle'
|
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,49 @@
|
|||
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=".App"
|
||||
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.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.AddSiteActivity"
|
||||
android:label="@string/add_site"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/AppTheme.Transparent"
|
||||
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.ViewSiteActivity"
|
||||
android:label="@string/view_site"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/AppTheme.Ink"
|
||||
android:windowSoftInputMode="stateHidden"/>
|
||||
|
||||
<service
|
||||
android:name="com.afollestad.nocknock.services.CheckService"
|
||||
android:enabled="true"
|
||||
android:label="Site Check Service" />
|
||||
<service
|
||||
android:name=".engine.statuscheck.CheckStatusJob"
|
||||
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>
|
||||
</application>
|
||||
|
||||
<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>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
56
app/src/main/java/com/afollestad/nocknock/App.kt
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.app.job.JobScheduler
|
||||
import android.content.Context
|
||||
import com.afollestad.nocknock.di.AppComponent
|
||||
import com.afollestad.nocknock.di.DaggerAppComponent
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob
|
||||
import com.afollestad.nocknock.ui.AddSiteActivity
|
||||
import com.afollestad.nocknock.ui.MainActivity
|
||||
import com.afollestad.nocknock.ui.ViewSiteActivity
|
||||
import com.afollestad.nocknock.utilities.Injector
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
class App : Application(), Injector {
|
||||
|
||||
lateinit var appComponent: AppComponent
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val okHttpClient = OkHttpClient.Builder()
|
||||
.addNetworkInterceptor { chain ->
|
||||
val request = chain.request()
|
||||
.newBuilder()
|
||||
.addHeader("User-Agent", "com.afollestad.nocknock")
|
||||
.build()
|
||||
chain.proceed(request)
|
||||
}
|
||||
.build()
|
||||
val jobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
|
||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
appComponent = DaggerAppComponent.builder()
|
||||
.application(this)
|
||||
.okHttpClient(okHttpClient)
|
||||
.jobScheduler(jobScheduler)
|
||||
.notificationManager(notificationManager)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun injectInto(target: Any) = when (target) {
|
||||
is MainActivity -> appComponent.inject(target)
|
||||
is ViewSiteActivity -> appComponent.inject(target)
|
||||
is AddSiteActivity -> appComponent.inject(target)
|
||||
is CheckStatusJob -> appComponent.inject(target)
|
||||
else -> throw IllegalStateException("Can't inject into $target")
|
||||
}
|
||||
}
|
|
@ -1,169 +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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private 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;
|
||||
|
||||
ServerVH(View itemView, ServerAdapter adapter) {
|
||||
super(itemView);
|
||||
iconStatus = itemView.findViewById(R.id.iconStatus);
|
||||
textName = itemView.findViewById(R.id.textName);
|
||||
textInterval = itemView.findViewById(R.id.textInterval);
|
||||
textUrl = itemView.findViewById(R.id.textUrl);
|
||||
textStatus = 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.textRes
|
||||
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: ServerModel, longClick: Boolean) -> Unit
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
class ServerVH constructor(
|
||||
itemView: View,
|
||||
private val adapter: ServerAdapter
|
||||
) : RecyclerView.ViewHolder(itemView), View.OnClickListener, View.OnLongClickListener {
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener(this)
|
||||
itemView.setOnLongClickListener(this)
|
||||
}
|
||||
|
||||
fun bind(model: ServerModel) {
|
||||
itemView.textName.text = model.name
|
||||
itemView.textUrl.text = model.url
|
||||
itemView.iconStatus.setStatus(model.status)
|
||||
|
||||
val statusText = model.status.textRes()
|
||||
if (statusText == 0) {
|
||||
itemView.textStatus.text = model.reason
|
||||
} else {
|
||||
itemView.textStatus.setText(statusText)
|
||||
}
|
||||
|
||||
itemView.textInterval.text = model.intervalText()
|
||||
}
|
||||
|
||||
override fun onClick(view: View) {
|
||||
adapter.performClick(adapterPosition, false)
|
||||
}
|
||||
|
||||
override fun onLongClick(view: View): Boolean {
|
||||
adapter.performClick(adapterPosition, true)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter<ServerVH>() {
|
||||
|
||||
private val models = mutableListOf<ServerModel>()
|
||||
|
||||
internal fun performClick(
|
||||
index: Int,
|
||||
longClick: Boolean
|
||||
) = listener.invoke(models[index], longClick)
|
||||
|
||||
fun add(model: ServerModel) {
|
||||
models.add(model)
|
||||
notifyItemInserted(models.size - 1)
|
||||
}
|
||||
|
||||
fun update(target: ServerModel) {
|
||||
for ((i, model) in models.withIndex()) {
|
||||
if (model.id == target.id) {
|
||||
update(i, target)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(
|
||||
index: Int,
|
||||
model: ServerModel
|
||||
) {
|
||||
models[index] = model
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
|
||||
fun remove(index: Int) {
|
||||
models.removeAt(index)
|
||||
notifyItemRemoved(index)
|
||||
}
|
||||
|
||||
fun remove(target: ServerModel) {
|
||||
for ((i, model) in models.withIndex()) {
|
||||
if (model.id == target.id) {
|
||||
remove(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun set(newModels: List<ServerModel>) {
|
||||
this.models.clear()
|
||||
if (newModels.isEmpty()) {
|
||||
return
|
||||
}
|
||||
this.models.addAll(newModels)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
models.clear()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): ServerVH {
|
||||
val v = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.list_item_server, parent, false)
|
||||
return ServerVH(v, this)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ServerVH,
|
||||
position: Int
|
||||
) {
|
||||
val model = models[position]
|
||||
holder.bind(model)
|
||||
}
|
||||
|
||||
override fun getItemCount() = models.size
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package com.afollestad.nocknock.api;
|
||||
|
||||
import static com.afollestad.nocknock.ui.MainActivity.SITES_TABLE_NAME;
|
||||
|
||||
import com.afollestad.inquiry.annotations.Column;
|
||||
import com.afollestad.inquiry.annotations.Table;
|
||||
import java.io.Serializable;
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
@Table(name = SITES_TABLE_NAME)
|
||||
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,18 +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 static final int OK = 1;
|
||||
public static final int WAITING = 2;
|
||||
public static final int CHECKING = 3;
|
||||
public static final int ERROR = 4;
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({OK, WAITING, CHECKING, ERROR})
|
||||
public @interface Enum {}
|
||||
}
|
|
@ -1,17 +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 static final int STATUS_CODE = 1;
|
||||
public static final int TERM_SEARCH = 2;
|
||||
public static final int JAVASCRIPT = 3;
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({STATUS_CODE, TERM_SEARCH, JAVASCRIPT})
|
||||
public @interface Enum {}
|
||||
}
|
56
app/src/main/java/com/afollestad/nocknock/di/AppComponent.kt
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.di
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.app.job.JobScheduler
|
||||
import com.afollestad.nocknock.engine.EngineModule
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob
|
||||
import com.afollestad.nocknock.notifications.NotificationsModule
|
||||
import com.afollestad.nocknock.ui.AddSiteActivity
|
||||
import com.afollestad.nocknock.ui.MainActivity
|
||||
import com.afollestad.nocknock.ui.ViewSiteActivity
|
||||
import com.afollestad.nocknock.utilities.UtilitiesModule
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
@Singleton
|
||||
@Component(
|
||||
modules = [
|
||||
MainModule::class,
|
||||
EngineModule::class,
|
||||
NotificationsModule::class,
|
||||
UtilitiesModule::class
|
||||
]
|
||||
)
|
||||
interface AppComponent {
|
||||
|
||||
fun inject(activity: MainActivity)
|
||||
|
||||
fun inject(activity: ViewSiteActivity)
|
||||
|
||||
fun inject(activity: AddSiteActivity)
|
||||
|
||||
fun inject(job: CheckStatusJob)
|
||||
|
||||
@Component.Builder
|
||||
interface Builder {
|
||||
|
||||
@BindsInstance fun application(application: Application): Builder
|
||||
|
||||
@BindsInstance fun okHttpClient(okHttpClient: OkHttpClient): Builder
|
||||
|
||||
@BindsInstance fun jobScheduler(jobScheduler: JobScheduler): Builder
|
||||
|
||||
@BindsInstance fun notificationManager(notificationManager: NotificationManager): Builder
|
||||
|
||||
fun build(): AppComponent
|
||||
}
|
||||
}
|
22
app/src/main/java/com/afollestad/nocknock/di/MainModule.kt
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.di
|
||||
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.utilities.qualifiers.AppIconRes
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
@Module
|
||||
open class MainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@AppIconRes
|
||||
fun provideAppIconRes(): Int = R.mipmap.ic_launcher
|
||||
}
|
|
@ -1,30 +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,32 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
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.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 {
|
||||
return MaterialDialog(activity!!)
|
||||
.title(R.string.about)
|
||||
.positiveButton(R.string.dismiss)
|
||||
.message(R.string.about_body, html = true, lineHeightMultiplier = 1.4f)
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package com.afollestad.nocknock.receivers;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
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 {
|
||||
|
||||
@SuppressLint("VisibleForTests")
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
|
||||
final Inquiry inq = Inquiry.newInstance(context, MainActivity.DB_NAME).build(false);
|
||||
ServerModel[] models = inq.select(ServerModel.class).all();
|
||||
AlarmUtil.setSiteChecks(context, models);
|
||||
inq.destroyInstance();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +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,235 +0,0 @@
|
|||
package com.afollestad.nocknock.services;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.IntentService;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
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 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")
|
||||
@SuppressLint("VisibleForTests")
|
||||
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(ServerModel.class).values(new ServerModel[] {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)
|
||||
.apply();
|
||||
}
|
||||
|
||||
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)
|
||||
.apply();
|
||||
}
|
||||
|
||||
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,275 +0,0 @@
|
|||
package com.afollestad.nocknock.ui;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.annotation.SuppressLint;
|
||||
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;
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_addsite);
|
||||
|
||||
rootLayout = findViewById(R.id.rootView);
|
||||
nameTiLayout = findViewById(R.id.nameTiLayout);
|
||||
inputName = findViewById(R.id.inputName);
|
||||
urlTiLayout = findViewById(R.id.urlTiLayout);
|
||||
inputUrl = findViewById(R.id.inputUrl);
|
||||
textUrlWarning = findViewById(R.id.textUrlWarning);
|
||||
inputInterval = findViewById(R.id.checkIntervalInput);
|
||||
spinnerInterval = findViewById(R.id.checkIntervalSpinner);
|
||||
responseValidationSpinner = findViewById(R.id.responseValidationMode);
|
||||
|
||||
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 = 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);
|
||||
}
|
||||
}
|
278
app/src/main/java/com/afollestad/nocknock/ui/AddSiteActivity.kt
Normal file
|
@ -0,0 +1,278 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Patterns.WEB_URL
|
||||
import android.view.View
|
||||
import android.view.ViewAnimationUtils.createCircularReveal
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.utilities.ext.conceal
|
||||
import com.afollestad.nocknock.utilities.ext.hide
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
import com.afollestad.nocknock.utilities.ext.onEnd
|
||||
import com.afollestad.nocknock.utilities.ext.onItemSelected
|
||||
import com.afollestad.nocknock.utilities.ext.onLayout
|
||||
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
|
||||
import com.afollestad.nocknock.utilities.ext.show
|
||||
import com.afollestad.nocknock.utilities.ext.showOrHide
|
||||
import com.afollestad.nocknock.utilities.ext.textAsLong
|
||||
import com.afollestad.nocknock.utilities.ext.trimmedText
|
||||
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalInput
|
||||
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalSpinner
|
||||
import kotlinx.android.synthetic.main.activity_addsite.doneBtn
|
||||
import kotlinx.android.synthetic.main.activity_addsite.inputName
|
||||
import kotlinx.android.synthetic.main.activity_addsite.inputUrl
|
||||
import kotlinx.android.synthetic.main.activity_addsite.nameTiLayout
|
||||
import kotlinx.android.synthetic.main.activity_addsite.responseValidationMode
|
||||
import kotlinx.android.synthetic.main.activity_addsite.responseValidationScript
|
||||
import kotlinx.android.synthetic.main.activity_addsite.responseValidationScriptInput
|
||||
import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchTerm
|
||||
import kotlinx.android.synthetic.main.activity_addsite.rootView
|
||||
import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning
|
||||
import kotlinx.android.synthetic.main.activity_addsite.toolbar
|
||||
import kotlinx.android.synthetic.main.activity_addsite.urlTiLayout
|
||||
import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.System.currentTimeMillis
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.max
|
||||
|
||||
private const val KEY_FAB_X = "fab_x"
|
||||
private const val KEY_FAB_Y = "fab_y"
|
||||
private const val KEY_FAB_SIZE = "fab_size"
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
fun MainActivity.intentToAdd(
|
||||
x: Float,
|
||||
y: Float,
|
||||
size: Int
|
||||
) = Intent(this, AddSiteActivity::class.java).apply {
|
||||
putExtra(KEY_FAB_X, x)
|
||||
putExtra(KEY_FAB_Y, y)
|
||||
putExtra(KEY_FAB_SIZE, size)
|
||||
addFlags(FLAG_ACTIVITY_NO_ANIMATION)
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
|
||||
|
||||
private var isClosing: Boolean = false
|
||||
|
||||
@Inject lateinit var serverModelStore: ServerModelStore
|
||||
@Inject lateinit var checkStatusManager: CheckStatusManager
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
injector().injectInto(this)
|
||||
|
||||
setContentView(R.layout.activity_addsite)
|
||||
toolbar.setNavigationOnClickListener { closeActivityWithReveal() }
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
rootView.conceal()
|
||||
rootView.onLayout { circularRevealActivity() }
|
||||
}
|
||||
|
||||
val intervalOptionsAdapter = ArrayAdapter(
|
||||
this,
|
||||
R.layout.list_item_spinner,
|
||||
resources.getStringArray(R.array.interval_options)
|
||||
)
|
||||
intervalOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
||||
checkIntervalSpinner.adapter = intervalOptionsAdapter
|
||||
|
||||
inputUrl.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) {
|
||||
val inputStr = inputUrl.text
|
||||
.toString()
|
||||
.trim()
|
||||
if (inputStr.isEmpty()) {
|
||||
return@setOnFocusChangeListener
|
||||
}
|
||||
|
||||
val uri = Uri.parse(inputStr)
|
||||
if (uri.scheme == null) {
|
||||
inputUrl.setText("http://$inputStr")
|
||||
textUrlWarning.hide()
|
||||
} else if ("http" != uri.scheme && "https" != uri.scheme) {
|
||||
textUrlWarning.show()
|
||||
textUrlWarning.setText(R.string.warning_http_url)
|
||||
} else {
|
||||
textUrlWarning.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
responseValidationMode.onItemSelected { pos ->
|
||||
responseValidationSearchTerm.showOrHide(pos == 1)
|
||||
responseValidationScript.showOrHide(pos == 2)
|
||||
|
||||
validationModeDescription.setText(
|
||||
when (pos) {
|
||||
0 -> R.string.validation_mode_status_desc
|
||||
1 -> R.string.validation_mode_term_desc
|
||||
2 -> R.string.validation_mode_javascript_desc
|
||||
else -> throw IllegalStateException("Unknown validation mode position: $pos")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
doneBtn.setOnClickListener(this)
|
||||
}
|
||||
|
||||
private fun closeActivityWithReveal() {
|
||||
if (isClosing) {
|
||||
return
|
||||
}
|
||||
|
||||
isClosing = true
|
||||
val fabSize = intent.getIntExtra(KEY_FAB_SIZE, toolbar!!.measuredHeight)
|
||||
|
||||
val defaultCx = rootView.measuredWidth / 2f
|
||||
val cx =
|
||||
intent.getFloatExtra(KEY_FAB_X, defaultCx).toInt() + fabSize / 2
|
||||
|
||||
val defaultCy = rootView.measuredHeight / 2f
|
||||
val cy = (intent.getFloatExtra(KEY_FAB_Y, defaultCy).toInt() +
|
||||
toolbar!!.measuredHeight +
|
||||
fabSize / 2)
|
||||
|
||||
val initialRadius = max(cx, cy).toFloat()
|
||||
createCircularReveal(rootView, cx, cy, initialRadius, 0f)
|
||||
.apply {
|
||||
duration = 300
|
||||
interpolator = AccelerateInterpolator()
|
||||
onEnd {
|
||||
rootView.conceal()
|
||||
finish()
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun circularRevealActivity() {
|
||||
val cx = rootView.measuredWidth / 2
|
||||
val cy = rootView.measuredHeight / 2
|
||||
val finalRadius = Math.max(cx, cy)
|
||||
.toFloat()
|
||||
val circularReveal = createCircularReveal(rootView, cx, cy, 0f, finalRadius)
|
||||
.apply {
|
||||
duration = 300
|
||||
interpolator = DecelerateInterpolator()
|
||||
}
|
||||
rootView.show()
|
||||
circularReveal.start()
|
||||
}
|
||||
|
||||
// Done button
|
||||
override fun onClick(view: View) {
|
||||
isClosing = true
|
||||
|
||||
var model = ServerModel(
|
||||
name = inputName.trimmedText(),
|
||||
url = inputUrl.trimmedText(),
|
||||
status = WAITING,
|
||||
validationMode = STATUS_CODE
|
||||
)
|
||||
|
||||
if (model.name.isEmpty()) {
|
||||
nameTiLayout.error = getString(R.string.please_enter_name)
|
||||
isClosing = false
|
||||
return
|
||||
} else {
|
||||
nameTiLayout.error = null
|
||||
}
|
||||
|
||||
if (model.url.isEmpty()) {
|
||||
urlTiLayout.error = getString(R.string.please_enter_url)
|
||||
isClosing = false
|
||||
return
|
||||
} else {
|
||||
urlTiLayout.error = null
|
||||
if (!WEB_URL.matcher(model.url).find()) {
|
||||
urlTiLayout.error = getString(R.string.please_enter_valid_url)
|
||||
isClosing = false
|
||||
return
|
||||
} else {
|
||||
val uri = Uri.parse(model.url)
|
||||
if (uri.scheme == null) {
|
||||
model = model.copy(url = "http://${model.url}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val intervalValue = checkIntervalInput.textAsLong()
|
||||
|
||||
model = when (checkIntervalSpinner.selectedItemPosition) {
|
||||
0 -> model.copy(checkInterval = intervalValue * (60 * 1000))
|
||||
1 -> model.copy(checkInterval = intervalValue * (60 * 60 * 1000))
|
||||
2 -> model.copy(checkInterval = intervalValue * (60 * 60 * 24 * 1000))
|
||||
else -> model.copy(checkInterval = intervalValue * (60 * 60 * 24 * 7 * 1000))
|
||||
}
|
||||
model = model.copy(lastCheck = currentTimeMillis() - model.checkInterval)
|
||||
|
||||
when (responseValidationMode.selectedItemPosition) {
|
||||
0 -> {
|
||||
model = model.copy(validationMode = STATUS_CODE, validationContent = null)
|
||||
}
|
||||
1 -> {
|
||||
model = model.copy(
|
||||
validationMode = TERM_SEARCH,
|
||||
validationContent = responseValidationSearchTerm.trimmedText()
|
||||
)
|
||||
}
|
||||
2 -> {
|
||||
model = model.copy(
|
||||
validationMode = JAVASCRIPT,
|
||||
validationContent = responseValidationScriptInput.trimmedText()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
rootView.scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
val storedModel = async(IO) { serverModelStore.put(model) }.await()
|
||||
checkStatusManager.cancelCheck(storedModel)
|
||||
checkStatusManager.scheduleCheck(storedModel, rightNow = true)
|
||||
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
overridePendingTransition(R.anim.fade_out, R.anim.fade_out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() = closeActivityWithReveal()
|
||||
}
|
|
@ -1,319 +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.dialogs.AboutDialog;
|
||||
import com.afollestad.nocknock.services.CheckService;
|
||||
import com.afollestad.nocknock.util.AlarmUtil;
|
||||
import com.afollestad.nocknock.util.MathUtil;
|
||||
|
||||
@SuppressLint("VisibleForTests")
|
||||
public class MainActivity extends AppCompatActivity
|
||||
implements SwipeRefreshLayout.OnRefreshListener,
|
||||
View.OnClickListener,
|
||||
ServerAdapter.ClickListener {
|
||||
|
||||
private static final int ADD_SITE_RQ = 6969;
|
||||
private static final int VIEW_SITE_RQ = 6923;
|
||||
public static final String DB_NAME = "nock_nock";
|
||||
public static final 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 = findViewById(R.id.emptyText);
|
||||
|
||||
mList = findViewById(R.id.list);
|
||||
mList.setLayoutManager(new LinearLayoutManager(this));
|
||||
mList.setAdapter(mAdapter);
|
||||
mList.addItemDecoration(
|
||||
new android.support.v7.widget.DividerItemDecoration(
|
||||
this, android.support.v7.widget.DividerItemDecoration.VERTICAL));
|
||||
|
||||
mRefreshLayout = 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 = findViewById(R.id.fab);
|
||||
mFab.setOnClickListener(this);
|
||||
|
||||
Inquiry.newInstance(this, DB_NAME).build();
|
||||
Bridge.config().defaultHeader("User-Agent", getString(R.string.app_name) + " (Android)");
|
||||
}
|
||||
|
||||
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).apply();
|
||||
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(new ServerModel[] {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.delete(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));
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@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());
|
||||
}
|
||||
}
|
||||
}
|
260
app/src/main/java/com/afollestad/nocknock/ui/MainActivity.kt
Normal file
|
@ -0,0 +1,260 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.ObjectAnimator.ofFloat
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityOptions.makeSceneTransitionAnimation
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.View.X
|
||||
import android.view.View.Y
|
||||
import android.view.animation.PathInterpolator
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.list.listItems
|
||||
import com.afollestad.nocknock.BuildConfig
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.adapter.ServerAdapter
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.dialogs.AboutDialog
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
import com.afollestad.nocknock.utilities.ext.onEnd
|
||||
import com.afollestad.nocknock.utilities.ext.safeRegisterReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.safeUnregisterReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
|
||||
import com.afollestad.nocknock.utilities.ext.show
|
||||
import com.afollestad.nocknock.utilities.ext.showOrHide
|
||||
import com.afollestad.nocknock.utilities.util.MathUtil.bezierCurve
|
||||
import kotlinx.android.synthetic.main.activity_main.emptyText
|
||||
import kotlinx.android.synthetic.main.activity_main.fab
|
||||
import kotlinx.android.synthetic.main.activity_main.list
|
||||
import kotlinx.android.synthetic.main.activity_main.rootView
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
|
||||
companion object {
|
||||
private const val ADD_SITE_RQ = 6969
|
||||
private const val VIEW_SITE_RQ = 6923
|
||||
private const val REVEAL_DURATION = 250L
|
||||
|
||||
private fun log(message: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d("MainActivity", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var fabAnimator: ObjectAnimator? = null
|
||||
private var originalFabX: Float = 0.toFloat()
|
||||
private var originalFabY: Float = 0.toFloat()
|
||||
|
||||
private val intentReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
intent: Intent
|
||||
) {
|
||||
log("Received broadcast ${intent.action}")
|
||||
when (intent.action) {
|
||||
ACTION_STATUS_UPDATE -> {
|
||||
val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? ServerModel ?: return
|
||||
list.post { adapter.update(model) }
|
||||
}
|
||||
else -> throw IllegalStateException("Unexpected intent: ${intent.action}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Inject lateinit var serverModelStore: ServerModelStore
|
||||
@Inject lateinit var notificationManager: NockNotificationManager
|
||||
@Inject lateinit var checkStatusManager: CheckStatusManager
|
||||
|
||||
private lateinit var adapter: ServerAdapter
|
||||
|
||||
@SuppressLint("CommitPrefEdits")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
injector().injectInto(this)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
adapter = ServerAdapter(this::onSiteSelected)
|
||||
|
||||
list.layoutManager = LinearLayoutManager(this)
|
||||
list.adapter = adapter
|
||||
list.addItemDecoration(DividerItemDecoration(this, VERTICAL))
|
||||
|
||||
fab.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
notificationManager.setIsAppOpen(true)
|
||||
|
||||
val filter = IntentFilter().apply {
|
||||
addAction(ACTION_STATUS_UPDATE)
|
||||
}
|
||||
safeRegisterReceiver(intentReceiver, filter)
|
||||
|
||||
refreshModels()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
notificationManager.setIsAppOpen(false)
|
||||
safeUnregisterReceiver(intentReceiver)
|
||||
}
|
||||
|
||||
private fun refreshModels() {
|
||||
adapter.clear()
|
||||
emptyText.show()
|
||||
rootView.scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
val models = async(IO) { serverModelStore.get() }.await()
|
||||
adapter.set(models)
|
||||
emptyText.showOrHide(adapter.itemCount == 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_main, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.about) {
|
||||
AboutDialog.show(this)
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
// FAB clicked
|
||||
override fun onClick(view: View) {
|
||||
originalFabX = fab.x
|
||||
originalFabY = fab.y
|
||||
|
||||
fabAnimator?.cancel()
|
||||
fabAnimator = ofFloat(view, X, Y, bezierCurve(fab, list))
|
||||
.apply {
|
||||
interpolator = PathInterpolator(.5f, .5f)
|
||||
duration = REVEAL_DURATION
|
||||
onEnd {
|
||||
startActivityForResult(
|
||||
intentToAdd(originalFabX, originalFabY, fab.measuredWidth),
|
||||
ADD_SITE_RQ
|
||||
)
|
||||
fab.postDelayed(
|
||||
{
|
||||
fab.x = originalFabX
|
||||
fab.y = originalFabY
|
||||
},
|
||||
REVEAL_DURATION * 2
|
||||
)
|
||||
}
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?
|
||||
) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (resultCode == RESULT_OK) {
|
||||
refreshModels()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSiteSelected(
|
||||
model: ServerModel,
|
||||
longClick: Boolean
|
||||
) {
|
||||
if (longClick) {
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.options)
|
||||
listItems(R.array.site_long_options) { _, i, _ ->
|
||||
when (i) {
|
||||
0 -> {
|
||||
checkStatusManager.cancelCheck(model)
|
||||
checkStatusManager.scheduleCheck(model)
|
||||
}
|
||||
1 -> maybeRemoveSite(model) {
|
||||
adapter.remove(i)
|
||||
emptyText.showOrHide(adapter.itemCount == 0)
|
||||
}
|
||||
else -> throw IllegalStateException("Unexpected index: $i")
|
||||
}
|
||||
}
|
||||
negativeButton(android.R.string.cancel)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
startActivityForResult(
|
||||
intentToView(model),
|
||||
VIEW_SITE_RQ,
|
||||
makeSceneTransitionAnimation(this).toBundle()
|
||||
)
|
||||
}
|
||||
|
||||
private fun maybeRemoveSite(
|
||||
model: ServerModel,
|
||||
onRemoved: (() -> Unit)?
|
||||
) {
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.remove_site)
|
||||
message(
|
||||
text = HtmlCompat.fromHtml(
|
||||
context.getString(R.string.remove_site_prompt, model.name), FROM_HTML_MODE_LEGACY
|
||||
)
|
||||
)
|
||||
positiveButton(R.string.remove) {
|
||||
checkStatusManager.cancelCheck(model)
|
||||
notificationManager.cancelStatusNotifications()
|
||||
performRemoveSite(model, onRemoved)
|
||||
}
|
||||
negativeButton(android.R.string.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun performRemoveSite(
|
||||
model: ServerModel,
|
||||
onRemoved: (() -> Unit)?
|
||||
) {
|
||||
rootView.scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
async(IO) { serverModelStore.delete(model) }.await()
|
||||
onRemoved?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,359 +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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_viewsite);
|
||||
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
toolbar.setNavigationOnClickListener(view -> finish());
|
||||
toolbar.inflateMenu(R.menu.menu_viewsite);
|
||||
toolbar.setOnMenuItemClickListener(this);
|
||||
|
||||
iconStatus = findViewById(R.id.iconStatus);
|
||||
inputName = findViewById(R.id.inputName);
|
||||
inputUrl = findViewById(R.id.inputUrl);
|
||||
textUrlWarning = findViewById(R.id.textUrlWarning);
|
||||
inputCheckInterval = findViewById(R.id.checkIntervalInput);
|
||||
checkIntervalSpinner = findViewById(R.id.checkIntervalSpinner);
|
||||
textLastCheckResult = findViewById(R.id.textLastCheckResult);
|
||||
textNextCheck = findViewById(R.id.textNextCheck);
|
||||
responseValidationSpinner = 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 = 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();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("VisibleForTests")
|
||||
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(ServerModel.class).values(new ServerModel[] {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;
|
||||
}
|
||||
}
|
416
app/src/main/java/com/afollestad/nocknock/ui/ViewSiteActivity.kt
Normal file
|
@ -0,0 +1,416 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
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.util.Log
|
||||
import android.util.Patterns.WEB_URL
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.text.HtmlCompat
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.nocknock.BuildConfig
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.data.textRes
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
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 com.afollestad.nocknock.utilities.ext.formatDate
|
||||
import com.afollestad.nocknock.utilities.ext.hide
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
import com.afollestad.nocknock.utilities.ext.isHttpOrHttps
|
||||
import com.afollestad.nocknock.utilities.ext.onItemSelected
|
||||
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
|
||||
import com.afollestad.nocknock.utilities.ext.show
|
||||
import com.afollestad.nocknock.utilities.ext.showOrHide
|
||||
import com.afollestad.nocknock.utilities.ext.textAsLong
|
||||
import com.afollestad.nocknock.utilities.ext.trimmedText
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalInput
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalSpinner
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.doneBtn
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.iconStatus
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.inputName
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.inputUrl
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.responseValidationMode
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.responseValidationScript
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.responseValidationScriptInput
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.responseValidationSearchTerm
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.rootView
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.textLastCheckResult
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.toolbar
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.System.currentTimeMillis
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.ceil
|
||||
|
||||
private const val KEY_VIEW_MODEL = "site_model"
|
||||
|
||||
/** @author Aidan Follestad (afViewSiteActivityollestad) */
|
||||
fun MainActivity.intentToView(model: ServerModel) =
|
||||
Intent(this, ViewSiteActivity::class.java).apply {
|
||||
putExtra(KEY_VIEW_MODEL, model)
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
class ViewSiteActivity : AppCompatActivity(),
|
||||
View.OnClickListener,
|
||||
Toolbar.OnMenuItemClickListener {
|
||||
companion object {
|
||||
private fun log(message: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d("ViewSiteActivity", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var currentModel: ServerModel
|
||||
|
||||
@Inject lateinit var serverModelStore: ServerModelStore
|
||||
@Inject lateinit var notificationManager: NockNotificationManager
|
||||
@Inject lateinit var checkStatusManager: CheckStatusManager
|
||||
|
||||
private val intentReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
intent: Intent
|
||||
) {
|
||||
log("Received broadcast ${intent.action}")
|
||||
val model = intent.getSerializableExtra(KEY_VIEW_MODEL) as? ServerModel
|
||||
if (model != null) {
|
||||
this@ViewSiteActivity.currentModel = model
|
||||
update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
injector().injectInto(this)
|
||||
setContentView(R.layout.activity_viewsite)
|
||||
|
||||
toolbar.run {
|
||||
setNavigationOnClickListener { finish() }
|
||||
inflateMenu(R.menu.menu_viewsite)
|
||||
setOnMenuItemClickListener(this@ViewSiteActivity)
|
||||
}
|
||||
|
||||
val intervalOptionsAdapter = ArrayAdapter(
|
||||
this,
|
||||
R.layout.list_item_spinner,
|
||||
resources.getStringArray(R.array.interval_options)
|
||||
)
|
||||
intervalOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
||||
checkIntervalSpinner.adapter = intervalOptionsAdapter
|
||||
|
||||
inputUrl.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) {
|
||||
val inputStr = inputUrl.text
|
||||
.toString()
|
||||
.trim()
|
||||
if (inputStr.isEmpty()) {
|
||||
return@setOnFocusChangeListener
|
||||
}
|
||||
|
||||
val uri = Uri.parse(inputStr)
|
||||
if (uri.scheme == null) {
|
||||
inputUrl.setText("http://$inputStr")
|
||||
textUrlWarning.hide()
|
||||
} else if (!uri.isHttpOrHttps()) {
|
||||
textUrlWarning.show()
|
||||
textUrlWarning.setText(R.string.warning_http_url)
|
||||
} else {
|
||||
textUrlWarning.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
responseValidationMode.onItemSelected { pos ->
|
||||
responseValidationSearchTerm.showOrHide(pos == 1)
|
||||
responseValidationScript.showOrHide(pos == 2)
|
||||
|
||||
validationModeDescription.setText(
|
||||
when (pos) {
|
||||
0 -> R.string.validation_mode_status_desc
|
||||
1 -> R.string.validation_mode_term_desc
|
||||
2 -> R.string.validation_mode_javascript_desc
|
||||
else -> throw IllegalStateException("Unexpected position: $pos")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
currentModel = intent.getSerializableExtra(KEY_VIEW_MODEL) as ServerModel
|
||||
update()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
if (intent != null && intent.hasExtra(KEY_VIEW_MODEL)) {
|
||||
currentModel = intent.getSerializableExtra(KEY_VIEW_MODEL) as ServerModel
|
||||
update()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun update() = with(currentModel) {
|
||||
iconStatus.setStatus(this.status)
|
||||
inputName.setText(this.name)
|
||||
inputUrl.setText(this.url)
|
||||
|
||||
if (this.lastCheck == 0L) {
|
||||
textLastCheckResult.setText(R.string.none)
|
||||
} else {
|
||||
val statusText = this.status.textRes()
|
||||
textLastCheckResult.text = if (statusText == 0) {
|
||||
this.reason
|
||||
} else {
|
||||
getString(statusText)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.checkInterval == 0L) {
|
||||
textNextCheck.setText(R.string.none_turned_off)
|
||||
checkIntervalInput.setText("")
|
||||
checkIntervalSpinner.setSelection(0)
|
||||
} else {
|
||||
var lastCheck = this.lastCheck
|
||||
if (lastCheck == 0L) {
|
||||
lastCheck = currentTimeMillis()
|
||||
}
|
||||
textNextCheck.text = (lastCheck + this.checkInterval).formatDate()
|
||||
|
||||
when {
|
||||
this.checkInterval >= WEEK -> {
|
||||
checkIntervalInput.setText(
|
||||
ceil((this.checkInterval.toFloat() / WEEK).toDouble()).toInt().toString()
|
||||
)
|
||||
checkIntervalSpinner.setSelection(3)
|
||||
}
|
||||
this.checkInterval >= DAY -> {
|
||||
checkIntervalInput.setText(
|
||||
ceil((this.checkInterval.toFloat() / DAY.toFloat()).toDouble()).toInt().toString()
|
||||
)
|
||||
checkIntervalSpinner.setSelection(2)
|
||||
}
|
||||
this.checkInterval >= HOUR -> {
|
||||
checkIntervalInput.setText(
|
||||
ceil((this.checkInterval.toFloat() / HOUR.toFloat()).toDouble()).toInt().toString()
|
||||
)
|
||||
checkIntervalSpinner.setSelection(1)
|
||||
}
|
||||
this.checkInterval >= MINUTE -> {
|
||||
checkIntervalInput.setText(
|
||||
ceil((this.checkInterval.toFloat() / MINUTE.toFloat()).toDouble()).toInt().toString()
|
||||
)
|
||||
checkIntervalSpinner.setSelection(0)
|
||||
}
|
||||
else -> {
|
||||
checkIntervalInput.setText("0")
|
||||
checkIntervalSpinner.setSelection(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
responseValidationMode.setSelection(validationMode.value - 1)
|
||||
|
||||
when (this.validationMode) {
|
||||
TERM_SEARCH -> responseValidationSearchTerm.setText(this.validationContent ?: "")
|
||||
JAVASCRIPT -> responseValidationScriptInput.setText(this.validationContent ?: "")
|
||||
else -> {
|
||||
responseValidationSearchTerm.setText("")
|
||||
responseValidationScriptInput.setText("")
|
||||
}
|
||||
}
|
||||
|
||||
doneBtn.setOnClickListener(this@ViewSiteActivity)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
try {
|
||||
val filter = IntentFilter()
|
||||
filter.addAction(ACTION_STATUS_UPDATE)
|
||||
// filter.addAction(CheckService.ACTION_JOB_RUNNING);
|
||||
registerReceiver(intentReceiver, filter)
|
||||
} catch (t: Throwable) {
|
||||
t.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
try {
|
||||
unregisterReceiver(intentReceiver)
|
||||
} catch (t: Throwable) {
|
||||
t.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateModelFromInput(withValidation: Boolean) {
|
||||
currentModel = currentModel.copy(
|
||||
name = inputName.trimmedText(),
|
||||
url = inputUrl.trimmedText(),
|
||||
status = WAITING
|
||||
)
|
||||
|
||||
if (withValidation && currentModel.name.isEmpty()) {
|
||||
inputName.error = getString(R.string.please_enter_name)
|
||||
return
|
||||
} else {
|
||||
inputName.error = null
|
||||
}
|
||||
|
||||
if (withValidation && currentModel.url.isEmpty()) {
|
||||
inputUrl.error = getString(R.string.please_enter_url)
|
||||
return
|
||||
} else {
|
||||
inputUrl.error = null
|
||||
if (withValidation && !WEB_URL.matcher(currentModel.url).find()) {
|
||||
inputUrl.error = getString(R.string.please_enter_valid_url)
|
||||
return
|
||||
} else {
|
||||
val uri = Uri.parse(currentModel.url)
|
||||
if (uri.scheme == null) {
|
||||
currentModel = currentModel.copy(url = "http://${currentModel.url}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val intervalValue = checkIntervalInput.textAsLong()
|
||||
|
||||
currentModel = when (checkIntervalSpinner.selectedItemPosition) {
|
||||
0 -> currentModel.copy(checkInterval = intervalValue * (60 * 1000))
|
||||
1 -> currentModel.copy(checkInterval = intervalValue * (60 * 60 * 1000))
|
||||
2 -> currentModel.copy(checkInterval = intervalValue * (60 * 60 * 24 * 1000))
|
||||
else -> currentModel.copy(checkInterval = intervalValue * (60 * 60 * 24 * 7 * 1000))
|
||||
}
|
||||
|
||||
currentModel = currentModel.copy(
|
||||
lastCheck = currentTimeMillis() - currentModel.checkInterval
|
||||
)
|
||||
|
||||
when (responseValidationMode.selectedItemPosition) {
|
||||
0 -> {
|
||||
currentModel = currentModel.copy(
|
||||
validationMode = STATUS_CODE,
|
||||
validationContent = null
|
||||
)
|
||||
}
|
||||
1 -> {
|
||||
currentModel = currentModel.copy(
|
||||
validationMode = TERM_SEARCH,
|
||||
validationContent = responseValidationSearchTerm.trimmedText()
|
||||
)
|
||||
}
|
||||
2 -> {
|
||||
currentModel = currentModel.copy(
|
||||
validationMode = JAVASCRIPT,
|
||||
validationContent = responseValidationScriptInput.trimmedText()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save button
|
||||
override fun onClick(view: View) {
|
||||
rootView.scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
updateModelFromInput(true)
|
||||
async(IO) { serverModelStore.update(currentModel) }.await()
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.refresh -> {
|
||||
rootView.scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
updateModelFromInput(false)
|
||||
async(IO) { serverModelStore.update(currentModel) }.await()
|
||||
checkStatusManager.cancelCheck(currentModel)
|
||||
checkStatusManager.scheduleCheck(currentModel, rightNow = true)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.remove -> {
|
||||
maybeRemoveSite(currentModel) { finish() }
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun maybeRemoveSite(
|
||||
model: ServerModel,
|
||||
onRemoved: (() -> Unit)?
|
||||
) {
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.remove_site)
|
||||
message(
|
||||
text = HtmlCompat.fromHtml(
|
||||
context.getString(R.string.remove_site_prompt, model.name),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||
)
|
||||
)
|
||||
positiveButton(R.string.remove) {
|
||||
checkStatusManager.cancelCheck(model)
|
||||
notificationManager.cancelStatusNotifications()
|
||||
performRemoveSite(model, onRemoved)
|
||||
}
|
||||
negativeButton(android.R.string.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun performRemoveSite(
|
||||
model: ServerModel,
|
||||
onRemoved: (() -> Unit)?
|
||||
) {
|
||||
rootView.scopeWhileAttached(Main) {
|
||||
launch(coroutineContext) {
|
||||
async(IO) { serverModelStore.delete(model) }.await()
|
||||
onRemoved?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,61 +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 static final 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,68 +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,30 +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,16 +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,30 +0,0 @@
|
|||
package com.afollestad.nocknock.util;
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
public class TimeUtil {
|
||||
|
||||
public static final long SECOND = 1000;
|
||||
public static final long MINUTE = SECOND * 60;
|
||||
public static final long HOUR = MINUTE * 60;
|
||||
public static final long DAY = HOUR * 24;
|
||||
public static final long WEEK = DAY * 7;
|
||||
public static final 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,44 +0,0 @@
|
|||
package com.afollestad.nocknock.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v7.widget.AppCompatImageView;
|
||||
import android.util.AttributeSet;
|
||||
import com.afollestad.nocknock.R;
|
||||
import com.afollestad.nocknock.api.ServerStatus;
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
public class StatusImageView extends AppCompatImageView {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerStatus
|
||||
import com.afollestad.nocknock.data.ServerStatus.CHECKING
|
||||
import com.afollestad.nocknock.data.ServerStatus.ERROR
|
||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
class StatusImageView(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : AppCompatImageView(context, attrs) {
|
||||
|
||||
init {
|
||||
setStatus(OK)
|
||||
}
|
||||
|
||||
fun setStatus(status: ServerStatus) = when (status) {
|
||||
CHECKING, WAITING -> {
|
||||
setImageResource(R.drawable.status_progress)
|
||||
setBackgroundResource(R.drawable.yellow_circle)
|
||||
}
|
||||
ERROR -> {
|
||||
setImageResource(R.drawable.status_error)
|
||||
setBackgroundResource(R.drawable.red_circle)
|
||||
}
|
||||
OK -> {
|
||||
setImageResource(R.drawable.status_ok)
|
||||
setBackgroundResource(R.drawable.green_circle)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
<?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>
|
||||
<alpha
|
||||
android:duration="400"
|
||||
android:fromAlpha="1.0"
|
||||
android:interpolator="@android:anim/accelerate_interpolator"
|
||||
android:toAlpha="0.0"/>
|
||||
</set>
|
||||
|
|
|
@ -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="@color/dividerColor"/>
|
||||
</shape>
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<solid android:color="@color/md_green" />
|
||||
<solid android:color="@color/md_green"/>
|
||||
|
||||
<stroke android:color="#424242" />
|
||||
<stroke android:color="#424242"/>
|
||||
|
||||
<size
|
||||
android:width="@dimen/list_circle_size"
|
||||
android:height="@dimen/list_circle_size" />
|
||||
</shape>
|
||||
<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="#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"/>
|
||||
</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="#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>
|
||||
|
|
|
@ -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="#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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -2,11 +2,11 @@
|
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<solid android:color="@color/md_red" />
|
||||
<solid android:color="@color/md_red"/>
|
||||
|
||||
<stroke android:color="#424242"/>
|
||||
<stroke android:color="#424242"/>
|
||||
|
||||
<size
|
||||
android:width="@dimen/list_circle_size"
|
||||
android:height="@dimen/list_circle_size" />
|
||||
</shape>
|
||||
<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="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>
|
||||
<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>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
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>
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||
</vector>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
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>
|
||||
<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>
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<solid android:color="@color/md_yellow" />
|
||||
<solid android:color="@color/md_yellow"/>
|
||||
|
||||
<stroke android:color="#424242" />
|
||||
<stroke android:color="#424242"/>
|
||||
|
||||
<size
|
||||
android:width="@dimen/list_circle_size"
|
||||
android:height="@dimen/list_circle_size" />
|
||||
</shape>
|
||||
<size
|
||||
android:width="@dimen/list_circle_size"
|
||||
android:height="@dimen/list_circle_size"/>
|
||||
</shape>
|
||||
|
|
|
@ -1,242 +1,270 @@
|
|||
<?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: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"
|
||||
android:background="?colorPrimary"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
>
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
<androidx.appcompat.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"
|
||||
/>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
>
|
||||
|
||||
<LinearLayout
|
||||
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" />
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/content_inset"
|
||||
android:paddingLeft="@dimen/content_inset"
|
||||
android:paddingRight="@dimen/content_inset"
|
||||
>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/nameTiLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="-4dp"
|
||||
android:layout_marginRight="-4dp"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
>
|
||||
|
||||
<LinearLayout
|
||||
<com.google.android.material.textfield.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"
|
||||
/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/urlTiLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="-4dp"
|
||||
android:layout_marginRight="-4dp"
|
||||
android:layout_marginTop="@dimen/content_inset_half"
|
||||
>
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/inputUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android: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"
|
||||
/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textUrlWarning"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/list_text_spacing"
|
||||
android: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"
|
||||
tools:ignore="Autofill"
|
||||
/>
|
||||
|
||||
<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"
|
||||
tools:ignore="UnusedAttribute"
|
||||
>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/content_inset"
|
||||
android:paddingLeft="@dimen/content_inset"
|
||||
android:paddingRight="@dimen/content_inset">
|
||||
>
|
||||
|
||||
<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">
|
||||
<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"
|
||||
/>
|
||||
|
||||
<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" />
|
||||
<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="Autofill,LabelFor,RtlSymmetry"
|
||||
/>
|
||||
|
||||
</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" />
|
||||
<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>
|
||||
|
||||
</ScrollView>
|
||||
</HorizontalScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
<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"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
@ -2,104 +2,44 @@
|
|||
<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.MainActivity"
|
||||
>
|
||||
|
||||
<android.support.v4.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical"
|
||||
/>
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical" />
|
||||
<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-medium"
|
||||
android:gravity="center"
|
||||
android:text="@string/no_sites_added"
|
||||
android:textSize="@dimen/empty_text_size"
|
||||
android:textStyle="italic"
|
||||
/>
|
||||
|
||||
</android.support.v4.widget.SwipeRefreshLayout>
|
||||
<com.google.android.material.floatingactionbutton.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"
|
||||
/>
|
||||
|
||||
<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" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/swipeRefreshTutorial"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#BF000000"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/content_inset"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="2">
|
||||
|
||||
<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" />
|
||||
|
||||
<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" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -6,290 +6,324 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?colorPrimary"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
>
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
<androidx.appcompat.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"
|
||||
/>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
>
|
||||
|
||||
<LinearLayout
|
||||
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" />
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/content_inset"
|
||||
android:paddingLeft="@dimen/content_inset"
|
||||
android:paddingRight="@dimen/content_inset"
|
||||
android:paddingTop="@dimen/content_inset_half"
|
||||
>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
>
|
||||
|
||||
<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,UnusedAttribute"
|
||||
/>
|
||||
|
||||
<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">
|
||||
>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<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"
|
||||
tools:ignore="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" />
|
||||
<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"
|
||||
tools:ignore="UnusedAttribute"
|
||||
/>
|
||||
|
||||
<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" />
|
||||
|
||||
<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" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:background="@color/dividerColorDark" />
|
||||
|
||||
<TextView
|
||||
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_marginTop="@dimen/list_text_spacing"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="@dimen/medium_text_size"
|
||||
tools:text="Everything checks out!" />
|
||||
|
||||
<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"
|
||||
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" />
|
||||
|
||||
<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" />
|
||||
<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>
|
||||
|
||||
</ScrollView>
|
||||
</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"
|
||||
/>
|
||||
|
||||
<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"
|
||||
tools:ignore="UnusedAttribute"
|
||||
>
|
||||
|
||||
<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="Autofill,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"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
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_marginTop="@dimen/list_text_spacing"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="@dimen/medium_text_size"
|
||||
tools:text="Everything checks out!"
|
||||
/>
|
||||
|
||||
<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"
|
||||
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"
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?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"
|
||||
|
@ -8,80 +9,88 @@
|
|||
android:paddingBottom="@dimen/content_inset_less"
|
||||
android:paddingLeft="@dimen/content_inset"
|
||||
android:paddingRight="@dimen/content_inset"
|
||||
android:paddingTop="@dimen/content_inset_less">
|
||||
android:paddingTop="@dimen/content_inset_less"
|
||||
>
|
||||
|
||||
<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.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
|
||||
<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_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/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"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:singleLine="true"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="@dimen/caption_font_size"
|
||||
tools:text="1h"
|
||||
/>
|
||||
|
||||
<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: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/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:fontFamily="sans-serif"
|
||||
android:singleLine="true"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="@dimen/body_font_size"
|
||||
tools:text="Everything checks out!"
|
||||
/>
|
||||
|
||||
<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 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<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" />
|
||||
android:textSize="@dimen/body_font_size" />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<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"
|
||||
|
@ -7,4 +8,5 @@
|
|||
android:paddingLeft="@dimen/content_inset"
|
||||
android:paddingRight="@dimen/content_inset"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="@dimen/body_font_size" />
|
||||
android:textSize="@dimen/body_font_size"
|
||||
/>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<?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"/>
|
||||
</menu>
|
||||
|
|
|
@ -2,16 +2,16 @@
|
|||
<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/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/remove"
|
||||
android:icon="@drawable/ic_action_delete"
|
||||
android:title="@string/remove_site"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
</menu>
|
||||
</menu>
|
||||
|
|
22
app/src/main/res/values/arrays.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string-array name="interval_options">
|
||||
<item>Minute(s)</item>
|
||||
<item>Hour(s)</item>
|
||||
<item>Day(s)</item>
|
||||
<item>Week(s)</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="site_long_options" translatable="false">
|
||||
<item>@string/refresh_status</item>
|
||||
<item>@string/remove_site</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="response_validation_options">
|
||||
<item>Status Code</item>
|
||||
<item>Search Term</item>
|
||||
<item>JavaScript Evaluation</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
|
@ -1,16 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<color name="colorPrimary">#455A64</color>
|
||||
<color name="colorPrimaryDark">#37474F</color>
|
||||
<color name="colorAccent">#FF6E40</color>
|
||||
<color name="colorPrimary">#455A64</color>
|
||||
<color name="colorPrimaryDark">#37474F</color>
|
||||
<color name="colorAccent">#FF6E40</color>
|
||||
|
||||
<color name="dividerColor">#EEEEEE</color>
|
||||
<color name="dividerColor">#EEEEEE</color>
|
||||
|
||||
<color name="md_red">#E53935</color>
|
||||
<color name="md_yellow">#FDD835</color>
|
||||
<color name="md_green">#43A047</color>
|
||||
<color name="md_red">#E53935</color>
|
||||
<color name="md_yellow">#FDD835</color>
|
||||
<color name="md_green">#43A047</color>
|
||||
|
||||
<color name="dividerColorDark">#37474F</color>
|
||||
<color name="dividerColorDark">#37474F</color>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
<resources>
|
||||
|
||||
<dimen name="headline_font_size">24sp</dimen>
|
||||
<dimen name="title_font_size">20sp</dimen>
|
||||
<dimen name="medium_text_size">16sp</dimen>
|
||||
<dimen name="body_font_size">14sp</dimen>
|
||||
<dimen name="caption_font_size">12sp</dimen>
|
||||
<dimen name="title_font_size">20sp</dimen>
|
||||
<dimen name="medium_text_size">16sp</dimen>
|
||||
<dimen name="body_font_size">14sp</dimen>
|
||||
<dimen name="caption_font_size">12sp</dimen>
|
||||
<dimen name="empty_text_size">26sp</dimen>
|
||||
|
||||
<dimen name="content_inset_half">8dp</dimen>
|
||||
<dimen name="content_inset_less">12dp</dimen>
|
||||
<dimen name="content_inset">16dp</dimen>
|
||||
<dimen name="content_inset_more">24dp</dimen>
|
||||
<dimen name="content_inset_double">32dp</dimen>
|
||||
<dimen name="content_inset_half">8dp</dimen>
|
||||
<dimen name="content_inset_less">12dp</dimen>
|
||||
<dimen name="content_inset">16dp</dimen>
|
||||
<dimen name="content_inset_more">24dp</dimen>
|
||||
|
||||
<dimen name="list_circle_size">42dp</dimen>
|
||||
<dimen name="list_text_spacing">4dp</dimen>
|
||||
<dimen name="fab_elevation">4dp</dimen>
|
||||
<dimen name="fab_elevation_pressed">8dp</dimen>
|
||||
<dimen name="button_height">52dp</dimen>
|
||||
<dimen name="tutorial_button_width">300dp</dimen>
|
||||
<dimen name="code_font_size">14sp</dimen>
|
||||
<dimen name="list_circle_size">42dp</dimen>
|
||||
<dimen name="list_text_spacing">4dp</dimen>
|
||||
<dimen name="fab_elevation">4dp</dimen>
|
||||
<dimen name="fab_elevation_pressed">8dp</dimen>
|
||||
<dimen name="button_height">52dp</dimen>
|
||||
<dimen name="code_font_size">14sp</dimen>
|
||||
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -1,81 +1,55 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<string name="app_name" tools:ignore="Typos">Nock Nock (BETA)</string>
|
||||
<string name="app_name" tools:ignore="Typos">Nock Nock (BETA)</string>
|
||||
|
||||
<string name="everything_checks_out">Everything checks out!</string>
|
||||
<string name="something_wrong">Something\'s wrong! Tap for details.</string>
|
||||
<string name="checking_status">Checking status…</string>
|
||||
<string name="waiting">Waiting…</string>
|
||||
<string name="no_sites_added">No sites added!</string>
|
||||
|
||||
<string name="no_sites_added">No sites added!</string>
|
||||
|
||||
<string name="about">About</string>
|
||||
<string name="about_body"><![CDATA[
|
||||
<string name="about">About</string>
|
||||
<string name="about_body"><![CDATA[
|
||||
<b>Nock Nock</b>, a simple app designed by <b>Aidan Follestad</b>.<br/>
|
||||
<a href=\'https://aidanfollestad.com\'>Website</a>
|
||||
<a href=\'https://twitter.com/afollestad\'>Twitter</a>
|
||||
<a href=\'https://google.com/+AidanFollestad\'>Google+</a>
|
||||
<a href=\'https://github.com/afollestad\'>GitHub</a>
|
||||
<a href=\'https://www.linkedin.com/in/afollestad\'>LinkedIn</a>
|
||||
<br/><br/><i>Nock Nock is open source! Check out the <a href=\'https://github.com/afollestad/nock-nock\'>GitHub page</a>!</i>
|
||||
<br/>Icon by <a href=\'https://plus.google.com/+KevinAguilarC\'>Kevin Aguilar</a> of <b>221 Pixels</b>.
|
||||
]]></string>
|
||||
<string name="dismiss">Dismiss</string>
|
||||
<string name="add_site">Add Site</string>
|
||||
<string name="site_name">Site Name</string>
|
||||
<string name="site_url">Site URL</string>
|
||||
<string name="check_interval">Check Interval</string>
|
||||
<string name="done">Done</string>
|
||||
<string name="please_enter_name">Please enter a name!</string>
|
||||
<string name="please_enter_url">Please enter a URL.</string>
|
||||
<string name="please_enter_valid_url">Please enter a valid URL.</string>
|
||||
|
||||
<string name="timeout">Request timed out! Your server is probably down.</string>
|
||||
<string name="options">Options</string>
|
||||
<string name="already_checking_sites">Already checking sites!</string>
|
||||
<string name="remove_site">Remove Site</string>
|
||||
<string name="remove_site_prompt"><![CDATA[Remove <b>%1$s</b> from your sites?]]></string>
|
||||
<string name="remove">Remove</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="view_site">View Site</string>
|
||||
<string name="last_check_result">Last Check Result</string>
|
||||
<string name="next_check">Next Check</string>
|
||||
<string name="none_turned_off">None (turned off)</string>
|
||||
<string name="none">None</string>
|
||||
<string name="dismiss">Dismiss</string>
|
||||
<string name="add_site">Add Site</string>
|
||||
<string name="site_name">Site Name</string>
|
||||
<string name="site_url">Site URL</string>
|
||||
<string name="check_interval">Check Interval</string>
|
||||
<string name="done">Done</string>
|
||||
<string name="please_enter_name">Please enter a name!</string>
|
||||
<string name="please_enter_url">Please enter a URL.</string>
|
||||
<string name="please_enter_valid_url">Please enter a valid URL.</string>
|
||||
|
||||
<string name="refresh_status">Refresh Status</string>
|
||||
<string name="swipe_refresh_hint">Drag the list down to manually refresh site statuses! Otherwise, they will be updated automatically in the background on chosen intervals.</string>
|
||||
<string name="understood">Understood!</string>
|
||||
<string name="options">Options</string>
|
||||
<string name="already_checking_sites">Already checking sites!</string>
|
||||
<string name="remove_site">Remove Site</string>
|
||||
<string name="remove_site_prompt"><![CDATA[Remove <b>%1$s</b> from your sites?]]></string>
|
||||
<string name="remove">Remove</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="view_site">View Site</string>
|
||||
<string name="last_check_result">Last Check Result</string>
|
||||
<string name="next_check">Next Check</string>
|
||||
<string name="none_turned_off">None (turned off)</string>
|
||||
<string name="none">None</string>
|
||||
|
||||
<string name="warning_http_url">
|
||||
<string name="refresh_status">Refresh Status</string>
|
||||
|
||||
<string name="warning_http_url">
|
||||
Warning: this app checks for server availability with HTTP requests. It\'s recommended that you use an HTTP URL.
|
||||
</string>
|
||||
<string name="default_js">var responseObj = JSON.parse(response);\nreturn responseObj.success === true;</string>
|
||||
<string name="function_declaration">function validate(response) {</string>
|
||||
<string name="function_end">}</string>
|
||||
<string name="response_validation_mode">Response Validation Mode</string>
|
||||
<string name="search_term">Search term…</string>
|
||||
<string name="default_js">var responseObj = JSON.parse(response);\nreturn responseObj.success === true;</string>
|
||||
<string name="function_declaration">function validate(response) {</string>
|
||||
<string name="function_end">}</string>
|
||||
<string name="response_validation_mode">Response Validation Mode</string>
|
||||
<string name="search_term">Search term…</string>
|
||||
|
||||
<string name="validation_mode_status_desc">The HTTP status code is checked. If it\'s a successful status code, the site passes the check.</string>
|
||||
<string name="validation_mode_term_desc">The status code check is done first. If it\'s successful, the response body is checked. If it contains your search term, the site passes the check.</string>
|
||||
<string name="validation_mode_javascript_desc">The status code check is done first. If it\'s successful, the response body is passed to the JavaScript function above. If the function returns true, the site passes the check. Throw an exception to pass custom error messages to Nock Nock.</string>
|
||||
|
||||
<string-array name="interval_options">
|
||||
<item>Minute(s)</item>
|
||||
<item>Hour(s)</item>
|
||||
<item>Day(s)</item>
|
||||
<item>Week(s)</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="site_long_options" translatable="false">
|
||||
<item>@string/refresh_status</item>
|
||||
<item>@string/remove_site</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="response_validation_options">
|
||||
<item>Status Code</item>
|
||||
<item>Search Term</item>
|
||||
<item>JavaScript Evaluation</item>
|
||||
</string-array>
|
||||
<string name="validation_mode_status_desc">The HTTP status code is checked. If it\'s a successful status code, the site passes the check.</string>
|
||||
<string name="validation_mode_term_desc">The status code check is done first. If it\'s successful, the response body is checked. If it contains your search term, the site passes the check.</string>
|
||||
<string name="validation_mode_javascript_desc">The status code check is done first. If it\'s successful, the response body is passed to the JavaScript function above. If the function returns true, the site passes the check. Throw an exception to pass custom error messages to Nock Nock.</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
<resources>
|
||||
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="colorButtonNormal">@color/colorPrimaryDark</item>
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="colorButtonNormal">@color/colorPrimaryDark</item>
|
||||
|
||||
<item name="android:listDivider">@drawable/divider</item>
|
||||
<item name="android:listDivider">@drawable/divider</item>
|
||||
|
||||
<item name="android:textColorPrimary">#212121</item>
|
||||
<item name="android:textColorSecondary">#727272</item>
|
||||
</style>
|
||||
<item name="android:textColorPrimary">#212121</item>
|
||||
<item name="android:textColorSecondary">#727272</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Ink" parent="Theme.AppCompat.NoActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="colorButtonNormal">@color/colorPrimaryDark</item>
|
||||
<style name="AppTheme.Ink" parent="Theme.AppCompat.NoActionBar">
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="colorButtonNormal">@color/colorPrimaryDark</item>
|
||||
|
||||
<item name="android:listDivider">@drawable/divider</item>
|
||||
</style>
|
||||
<item name="android:listDivider">@drawable/divider</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Transparent" parent="AppTheme.Ink">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
</style>
|
||||
<style name="AppTheme.Transparent" parent="AppTheme.Ink">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="AccentButton" parent="Widget.AppCompat.Button.Colored">
|
||||
<item name="android:textColor">#fff</item>
|
||||
<item name="android:colorButtonNormal">@color/colorAccent</item>
|
||||
</style>
|
||||
<style name="AccentButton" parent="Widget.AppCompat.Button.Colored">
|
||||
<item name="android:textColor">#fff</item>
|
||||
<item name="android:colorButtonNormal">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
50
build.gradle
|
@ -1,38 +1,30 @@
|
|||
apply from: './dependencies.gradle'
|
||||
apply from: './versionsPlugin.gradle'
|
||||
|
||||
buildscript {
|
||||
apply from: './dependencies.gradle'
|
||||
apply from: './dependencies.gradle'
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:' + versions.gradlePlugin
|
||||
classpath "com.diffplug.spotless:spotless-plugin-gradle:" + versions.spotlessPlugin
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:' + versions.gradlePlugin
|
||||
classpath 'com.diffplug.spotless:spotless-plugin-gradle:' + versions.spotlessPlugin
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:' + versions.kotlin
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:' + versions.versionPlugin
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url "https://dl.bintray.com/drummer-aidan/maven" }
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
}
|
||||
}
|
||||
apply plugin: "com.diffplug.gradle.spotless"
|
||||
spotless {
|
||||
java {
|
||||
target "**/*.java"
|
||||
trimTrailingWhitespace()
|
||||
removeUnusedImports()
|
||||
googleJavaFormat()
|
||||
endWithNewline()
|
||||
}
|
||||
}
|
||||
tasks.withType(Javadoc).all {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
|
|
1
data/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
22
data/build.gradle
Normal file
|
@ -0,0 +1,22 @@
|
|||
apply from: '../dependencies.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion versions.minSdk
|
||||
targetSdkVersion versions.compileSdk
|
||||
versionCode versions.publishVersionCode
|
||||
versionName versions.publishVersion
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':utilities')
|
||||
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin
|
||||
}
|
||||
|
||||
apply from: '../spotless.gradle'
|
2
data/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<manifest
|
||||
package="com.afollestad.nocknock.data"/>
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.data
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
||||
import com.afollestad.nocknock.utilities.ext.timeString
|
||||
import java.io.Serializable
|
||||
import java.lang.System.currentTimeMillis
|
||||
|
||||
/** @author Aidan Follestad (afollestad)*/
|
||||
data class ServerModel(
|
||||
var id: Int = 0,
|
||||
val name: String = "Unknown",
|
||||
val url: String = "Unknown",
|
||||
val status: ServerStatus = OK,
|
||||
val checkInterval: Long = 0,
|
||||
val lastCheck: Long = 0,
|
||||
val reason: String? = null,
|
||||
val validationMode: ValidationMode,
|
||||
val validationContent: String? = null
|
||||
) : Serializable {
|
||||
|
||||
companion object {
|
||||
const val TABLE_NAME = "server_models"
|
||||
const val COLUMN_ID = "_id"
|
||||
const val COLUMN_NAME = "name"
|
||||
const val COLUMN_URL = "url"
|
||||
const val COLUMN_STATUS = "status"
|
||||
const val COLUMN_CHECK_INTERVAL = "check_interval"
|
||||
const val COLUMN_LAST_CHECK = "last_check"
|
||||
const val COLUMN_REASON = "reason"
|
||||
const val COLUMN_VALIDATION_MODE = "validation_mode"
|
||||
const val COLUMN_VALIDATION_CONTENT = "validation_content"
|
||||
|
||||
const val DEFAULT_SORT_ORDER = "$COLUMN_NAME ASC"
|
||||
|
||||
fun pull(cursor: Cursor): ServerModel {
|
||||
return ServerModel(
|
||||
id = cursor.getInt(cursor.getColumnIndex(COLUMN_ID)),
|
||||
name = cursor.getString(cursor.getColumnIndex(COLUMN_NAME)),
|
||||
url = cursor.getString(cursor.getColumnIndex(COLUMN_URL)),
|
||||
status = cursor.getInt(cursor.getColumnIndex(COLUMN_STATUS)).toServerStatus(),
|
||||
checkInterval = cursor.getLong(cursor.getColumnIndex(COLUMN_CHECK_INTERVAL)),
|
||||
lastCheck = cursor.getLong(cursor.getColumnIndex(COLUMN_LAST_CHECK)),
|
||||
reason = cursor.getString(cursor.getColumnIndex(COLUMN_REASON)),
|
||||
validationMode = cursor.getInt(
|
||||
cursor.getColumnIndex(COLUMN_VALIDATION_MODE)
|
||||
).toValidationMode(),
|
||||
validationContent = cursor.getString(cursor.getColumnIndex(COLUMN_VALIDATION_CONTENT))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun intervalText() = if (checkInterval <= 0) {
|
||||
""
|
||||
} else {
|
||||
val now = currentTimeMillis()
|
||||
val nextCheck = lastCheck + checkInterval
|
||||
(nextCheck - now).timeString()
|
||||
}
|
||||
|
||||
fun toContentValues() = ContentValues().apply {
|
||||
put(COLUMN_NAME, name)
|
||||
put(COLUMN_URL, url)
|
||||
put(COLUMN_STATUS, status.value)
|
||||
put(COLUMN_CHECK_INTERVAL, checkInterval)
|
||||
put(COLUMN_LAST_CHECK, lastCheck)
|
||||
put(COLUMN_REASON, reason)
|
||||
put(COLUMN_VALIDATION_MODE, validationMode.value)
|
||||
put(COLUMN_VALIDATION_CONTENT, validationContent)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.data
|
||||
|
||||
import com.afollestad.nocknock.data.ServerStatus.CHECKING
|
||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
enum class ServerStatus(val value: Int) {
|
||||
OK(1),
|
||||
WAITING(2),
|
||||
CHECKING(3),
|
||||
ERROR(4);
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromValue(value: Int) = when (value) {
|
||||
OK.value -> OK
|
||||
WAITING.value -> WAITING
|
||||
CHECKING.value -> CHECKING
|
||||
ERROR.value -> ERROR
|
||||
else -> throw IllegalArgumentException("Unknown validationMode: $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ServerStatus.textRes() = when (this) {
|
||||
OK -> R.string.everything_checks_out
|
||||
WAITING -> R.string.waiting
|
||||
CHECKING -> R.string.checking_status
|
||||
else -> 0
|
||||
}
|
||||
|
||||
fun Int.toServerStatus() = ServerStatus.fromValue(this)
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.data
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
enum class ValidationMode(val value: Int) {
|
||||
STATUS_CODE(1),
|
||||
TERM_SEARCH(2),
|
||||
JAVASCRIPT(3);
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromValue(value: Int) = when (value) {
|
||||
STATUS_CODE.value -> STATUS_CODE
|
||||
TERM_SEARCH.value -> TERM_SEARCH
|
||||
JAVASCRIPT.value -> JAVASCRIPT
|
||||
else -> throw IllegalArgumentException("Unknown validationMode: $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Int.toValidationMode() = ValidationMode.fromValue(this)
|
7
data/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<resources>
|
||||
|
||||
<string name="everything_checks_out">Everything checks out!</string>
|
||||
<string name="checking_status">Checking status…</string>
|
||||
<string name="waiting">Waiting…</string>
|
||||
|
||||
</resources>
|
|
@ -1,13 +1,29 @@
|
|||
ext.versions = [
|
||||
minSdk : 21,
|
||||
compileSdk : 27,
|
||||
buildTools : '27.0.2',
|
||||
publishVersion : '0.1.3.1',
|
||||
publishVersionCode: 14,
|
||||
gradlePlugin : '3.0.1',
|
||||
spotlessPlugin : '3.8.0',
|
||||
supportLib : '27.0.2',
|
||||
materialDialogs : '0.9.6.0',
|
||||
bridge : '5.1.2',
|
||||
inquiry : '5.0.0'
|
||||
]
|
||||
minSdk : 21,
|
||||
compileSdk : 28,
|
||||
buildTools : '28.0.3',
|
||||
publishVersion : '0.7.1',
|
||||
publishVersionCode: 27,
|
||||
|
||||
gradlePlugin : '3.2.1',
|
||||
spotlessPlugin : '3.16.0',
|
||||
versionPlugin : '0.20.0',
|
||||
|
||||
okHttp : '3.12.0',
|
||||
rhino : '1.7.10',
|
||||
|
||||
dagger : '2.19',
|
||||
kotlin : '1.3.10',
|
||||
coroutines : '1.0.1',
|
||||
androidx : '1.0.0',
|
||||
|
||||
rxBinding : '3.0.0-alpha1',
|
||||
|
||||
materialDialogs : '2.0.0-rc3',
|
||||
rxkPrefs : '1.2.0',
|
||||
|
||||
junit : '4.12',
|
||||
mockito : '2.23.0',
|
||||
mockitoKotlin : '2.0.0-RC1',
|
||||
truth : '0.42'
|
||||
]
|
||||
|
|
1
engine/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
36
engine/build.gradle
Normal file
|
@ -0,0 +1,36 @@
|
|||
apply from: '../dependencies.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion versions.minSdk
|
||||
targetSdkVersion versions.compileSdk
|
||||
versionCode versions.publishVersionCode
|
||||
versionName versions.publishVersion
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':utilities')
|
||||
implementation project(':data')
|
||||
implementation project(':notifications')
|
||||
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin
|
||||
api 'org.jetbrains.kotlinx:kotlinx-coroutines-core:' + versions.coroutines
|
||||
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:' + versions.coroutines
|
||||
|
||||
api 'com.squareup.okhttp3:okhttp:' + versions.okHttp
|
||||
|
||||
implementation 'com.google.dagger:dagger:' + versions.dagger
|
||||
kapt 'com.google.dagger:dagger-compiler:' + versions.dagger
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
apply from: '../spotless.gradle'
|
5
engine/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.afollestad.nocknock.engine">
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
</manifest>
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.engine
|
||||
|
||||
import com.afollestad.nocknock.engine.db.RealServerModelStore
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||
import com.afollestad.nocknock.engine.statuscheck.RealCheckStatusManager
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
@Module
|
||||
abstract class EngineModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideServerModelStore(
|
||||
serverModelStore: RealServerModelStore
|
||||
): ServerModelStore
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideCheckStatusManager(
|
||||
checkStatusManager: RealCheckStatusManager
|
||||
): CheckStatusManager
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.engine.db
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
|
||||
private const val SQL_CREATE_ENTRIES =
|
||||
"CREATE TABLE ${ServerModel.TABLE_NAME} (" +
|
||||
"${ServerModel.COLUMN_ID} INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
|
||||
"${ServerModel.COLUMN_NAME} TEXT," +
|
||||
"${ServerModel.COLUMN_URL} TEXT," +
|
||||
"${ServerModel.COLUMN_STATUS} INTEGER," +
|
||||
"${ServerModel.COLUMN_CHECK_INTERVAL} INTEGER," +
|
||||
"${ServerModel.COLUMN_LAST_CHECK} INTEGER," +
|
||||
"${ServerModel.COLUMN_REASON} TEXT," +
|
||||
"${ServerModel.COLUMN_VALIDATION_MODE} INTEGER," +
|
||||
"${ServerModel.COLUMN_VALIDATION_CONTENT} TEXT)"
|
||||
|
||||
private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${ServerModel.TABLE_NAME}"
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
class ServerModelDbHelper(context: Context) : SQLiteOpenHelper(
|
||||
context, DATABASE_NAME, null, DATABASE_VERSION
|
||||
) {
|
||||
companion object {
|
||||
const val DATABASE_VERSION = 1
|
||||
const val DATABASE_NAME = "ServerModels.db"
|
||||
}
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
db.execSQL(SQL_CREATE_ENTRIES)
|
||||
}
|
||||
|
||||
override fun onUpgrade(
|
||||
db: SQLiteDatabase,
|
||||
oldVersion: Int,
|
||||
newVersion: Int
|
||||
) {
|
||||
db.execSQL(SQL_DELETE_ENTRIES)
|
||||
onCreate(db)
|
||||
}
|
||||
|
||||
override fun onDowngrade(
|
||||
db: SQLiteDatabase,
|
||||
oldVersion: Int,
|
||||
newVersion: Int
|
||||
) = onUpgrade(db, oldVersion, newVersion)
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.engine.db
|
||||
|
||||
import android.app.Application
|
||||
import android.database.Cursor
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerModel.Companion.COLUMN_ID
|
||||
import com.afollestad.nocknock.data.ServerModel.Companion.DEFAULT_SORT_ORDER
|
||||
import com.afollestad.nocknock.data.ServerModel.Companion.TABLE_NAME
|
||||
import com.afollestad.nocknock.utilities.ext.diffFrom
|
||||
import javax.inject.Inject
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
interface ServerModelStore {
|
||||
|
||||
suspend fun get(id: Int? = null): List<ServerModel>
|
||||
|
||||
suspend fun put(model: ServerModel): ServerModel
|
||||
|
||||
suspend fun update(model: ServerModel): Int
|
||||
|
||||
suspend fun delete(model: ServerModel): Int
|
||||
|
||||
suspend fun delete(id: Int): Int
|
||||
|
||||
suspend fun deleteAll(): Int
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
class RealServerModelStore @Inject constructor(
|
||||
app: Application
|
||||
) : ServerModelStore {
|
||||
|
||||
private val dbHelper = ServerModelDbHelper(app)
|
||||
|
||||
override suspend fun get(id: Int?): List<ServerModel> {
|
||||
if (id == null) {
|
||||
return getAll()
|
||||
}
|
||||
|
||||
val reader = dbHelper.readableDatabase
|
||||
val selection = "$COLUMN_ID = ?"
|
||||
val selectionArgs = arrayOf("$id")
|
||||
val cursor = reader.query(
|
||||
TABLE_NAME,
|
||||
null,
|
||||
selection,
|
||||
selectionArgs,
|
||||
null,
|
||||
null,
|
||||
DEFAULT_SORT_ORDER,
|
||||
"1"
|
||||
)
|
||||
cursor.use {
|
||||
val results = readModels(it)
|
||||
check(results.size == 1) { "Should only get one model per ID." }
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAll(): List<ServerModel> {
|
||||
val reader = dbHelper.readableDatabase
|
||||
val cursor = reader.query(
|
||||
TABLE_NAME,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
DEFAULT_SORT_ORDER,
|
||||
null
|
||||
)
|
||||
cursor.use { return readModels(it) }
|
||||
}
|
||||
|
||||
override suspend fun put(model: ServerModel): ServerModel {
|
||||
check(model.id == 0) { "Cannot put a model that already has an ID." }
|
||||
|
||||
val writer = dbHelper.writableDatabase
|
||||
val newId = writer.insert(TABLE_NAME, null, model.toContentValues())
|
||||
|
||||
return model.copy(id = newId.toInt())
|
||||
}
|
||||
|
||||
override suspend fun update(model: ServerModel): Int {
|
||||
check(model.id != 0) { "Cannot update a model that does not have an ID." }
|
||||
|
||||
val oldModel = get(model.id).single()
|
||||
val oldValues = oldModel.toContentValues()
|
||||
|
||||
val writer = dbHelper.writableDatabase
|
||||
val newValues = model.toContentValues()
|
||||
val valuesDiff = oldValues.diffFrom(newValues)
|
||||
|
||||
val selection = "$COLUMN_ID = ?"
|
||||
val selectionArgs = arrayOf("${model.id}")
|
||||
|
||||
return writer.update(TABLE_NAME, valuesDiff, selection, selectionArgs)
|
||||
}
|
||||
|
||||
override suspend fun delete(model: ServerModel) = delete(model.id)
|
||||
|
||||
override suspend fun delete(id: Int): Int {
|
||||
check(id != 0) { "Cannot delete a model that doesn't have an ID." }
|
||||
|
||||
val selection = "$COLUMN_ID = ?"
|
||||
val selectionArgs = arrayOf("$id")
|
||||
return dbHelper.writableDatabase.delete(TABLE_NAME, selection, selectionArgs)
|
||||
}
|
||||
|
||||
override suspend fun deleteAll(): Int {
|
||||
return dbHelper.writableDatabase.delete(TABLE_NAME, null, null)
|
||||
}
|
||||
|
||||
private fun readModels(cursor: Cursor): List<ServerModel> {
|
||||
val results = mutableListOf<ServerModel>()
|
||||
while (cursor.moveToNext()) {
|
||||
results.add(ServerModel.pull(cursor))
|
||||
}
|
||||
return results
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.engine.statuscheck
|
||||
|
||||
import android.app.job.JobParameters
|
||||
import android.app.job.JobService
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus
|
||||
import com.afollestad.nocknock.data.ServerStatus.CHECKING
|
||||
import com.afollestad.nocknock.data.ServerStatus.ERROR
|
||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||
import com.afollestad.nocknock.engine.BuildConfig.APPLICATION_ID
|
||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||
import com.afollestad.nocknock.utilities.BuildConfig
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
import com.afollestad.nocknock.utilities.js.JavaScript
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.System.currentTimeMillis
|
||||
import javax.inject.Inject
|
||||
|
||||
/** @author Aidan Follestad (afollestad)*/
|
||||
class CheckStatusJob : JobService() {
|
||||
|
||||
companion object {
|
||||
const val ACTION_STATUS_UPDATE = "$APPLICATION_ID.STATUS_UPDATE"
|
||||
const val ACTION_JOB_RUNNING = "$APPLICATION_ID.STATUS_JOB_RUNNING"
|
||||
const val KEY_UPDATE_MODEL = "site_model"
|
||||
const val KEY_SITE_ID = "site.id"
|
||||
|
||||
private fun log(message: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d("CheckStatusJob", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Inject lateinit var modelStore: ServerModelStore
|
||||
@Inject lateinit var checkStatusManager: CheckStatusManager
|
||||
@Inject lateinit var notificationManager: NockNotificationManager
|
||||
|
||||
override fun onStartJob(params: JobParameters): Boolean {
|
||||
injector().injectInto(this)
|
||||
val siteId = params.extras.getInt(KEY_SITE_ID)
|
||||
|
||||
GlobalScope.launch(Main) {
|
||||
val sites = async(IO) { modelStore.get(id = siteId) }.await()
|
||||
if (sites.isEmpty()) {
|
||||
log("Unable to find any sites for ID $siteId, this job will not be rescheduled.")
|
||||
return@launch jobFinished(params, false)
|
||||
}
|
||||
|
||||
val site = sites.single()
|
||||
log("Performing status checks on site ${site.id}...")
|
||||
sendBroadcast(Intent(ACTION_JOB_RUNNING).apply { putExtra(KEY_SITE_ID, site.id) })
|
||||
|
||||
log("Checking ${site.name} (${site.url})...")
|
||||
|
||||
val result = async(IO) {
|
||||
updateStatus(site, CHECKING)
|
||||
val checkResult = checkStatusManager.performCheck(site)
|
||||
val resultModel = checkResult.model
|
||||
val resultResponse = checkResult.response
|
||||
|
||||
if (resultModel.status != OK) {
|
||||
log("Got unsuccessful check status back: ${resultModel.reason}")
|
||||
return@async updateStatus(site = resultModel)
|
||||
} else {
|
||||
when (site.validationMode) {
|
||||
TERM_SEARCH -> {
|
||||
val body = resultResponse?.body()?.string() ?: ""
|
||||
log("Using TERM_SEARCH validation mode on body of length: ${body.length}")
|
||||
|
||||
return@async if (!body.contains(site.validationContent ?: "")) {
|
||||
updateStatus(
|
||||
resultModel.copy(
|
||||
status = ERROR,
|
||||
reason = "Term \"${site.validationContent}\" not found in response body."
|
||||
)
|
||||
)
|
||||
} else {
|
||||
resultModel
|
||||
}
|
||||
}
|
||||
JAVASCRIPT -> {
|
||||
val body = resultResponse?.body()?.string() ?: ""
|
||||
log("Using JAVASCRIPT validation mode on body of length: ${body.length}")
|
||||
val reason = JavaScript.eval(resultModel.validationContent ?: "", body)
|
||||
return@async if (reason != null) {
|
||||
updateStatus(resultModel.copy(reason = reason), status = ERROR)
|
||||
} else {
|
||||
resultModel
|
||||
}
|
||||
}
|
||||
STATUS_CODE -> {
|
||||
// We already know the status code is successful because we are in this else branch
|
||||
updateStatus(
|
||||
resultModel.copy(
|
||||
status = OK,
|
||||
reason = null
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
throw IllegalArgumentException("Unknown validation mode: ${site.validationMode}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}.await()
|
||||
|
||||
if (result.status == OK) {
|
||||
notificationManager.cancelStatusNotification(result)
|
||||
} else {
|
||||
notificationManager.postStatusNotification(result)
|
||||
}
|
||||
|
||||
checkStatusManager.scheduleCheck(result)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onStopJob(params: JobParameters): Boolean {
|
||||
val siteId = params.extras.getInt(KEY_SITE_ID)
|
||||
log("Check job for site $siteId is done")
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun updateStatus(
|
||||
site: ServerModel,
|
||||
status: ServerStatus = site.status
|
||||
): ServerModel {
|
||||
log("Updating ${site.name} (${site.url}) status to $status...")
|
||||
|
||||
val lastCheckTime =
|
||||
if (status == CHECKING) currentTimeMillis()
|
||||
else site.lastCheck
|
||||
val reason =
|
||||
if (status == OK) null
|
||||
else site.reason
|
||||
|
||||
val newSiteModel = site.copy(
|
||||
status = status,
|
||||
lastCheck = lastCheckTime,
|
||||
reason = reason
|
||||
)
|
||||
modelStore.update(newSiteModel)
|
||||
|
||||
withContext(Main) {
|
||||
sendBroadcast(Intent(ACTION_STATUS_UPDATE).apply { putExtra(KEY_UPDATE_MODEL, site) })
|
||||
}
|
||||
return newSiteModel
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.engine.statuscheck
|
||||
|
||||
import android.app.Application
|
||||
import android.app.job.JobInfo.NETWORK_TYPE_ANY
|
||||
import android.app.job.JobScheduler
|
||||
import android.app.job.JobScheduler.RESULT_SUCCESS
|
||||
import android.os.PersistableBundle
|
||||
import android.util.Log
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus.ERROR
|
||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
||||
import com.afollestad.nocknock.engine.R
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_SITE_ID
|
||||
import com.afollestad.nocknock.utilities.BuildConfig
|
||||
import com.afollestad.nocknock.utilities.providers.StringProvider
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import java.net.SocketTimeoutException
|
||||
import javax.inject.Inject
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
data class CheckResult(
|
||||
val model: ServerModel,
|
||||
val response: Response? = null
|
||||
)
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
interface CheckStatusManager {
|
||||
|
||||
fun scheduleCheck(
|
||||
site: ServerModel,
|
||||
rightNow: Boolean = false
|
||||
)
|
||||
|
||||
fun cancelCheck(site: ServerModel)
|
||||
|
||||
suspend fun performCheck(site: ServerModel): CheckResult
|
||||
}
|
||||
|
||||
class RealCheckStatusManager @Inject constructor(
|
||||
private val app: Application,
|
||||
private val jobScheduler: JobScheduler,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val stringProvider: StringProvider
|
||||
) : CheckStatusManager {
|
||||
|
||||
companion object {
|
||||
|
||||
private fun log(message: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d("CheckStatusManager", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun scheduleCheck(
|
||||
site: ServerModel,
|
||||
rightNow: Boolean
|
||||
) {
|
||||
check(site.id != 0) { "Cannot schedule checks for jobs with no ID." }
|
||||
log("Requesting a check job for site to be scheduled: $site")
|
||||
|
||||
val extras = PersistableBundle().apply {
|
||||
putInt(KEY_SITE_ID, site.id)
|
||||
}
|
||||
|
||||
// Note that we don't use the periodic feature of JobScheduler because it requires a
|
||||
// minimum of 15 minutes between each execution which may not be what's requested by the
|
||||
// user of this app.
|
||||
val jobInfo = jobInfo(app, site.id, CheckStatusJob::class.java) {
|
||||
setRequiredNetworkType(NETWORK_TYPE_ANY)
|
||||
if (rightNow) {
|
||||
log(">> Job for site ${site.id} should be executed now")
|
||||
setMinimumLatency(1)
|
||||
} else {
|
||||
log(">> Job for site ${site.id} should be in ${site.checkInterval}ms")
|
||||
setMinimumLatency(site.checkInterval)
|
||||
}
|
||||
setExtras(extras)
|
||||
setPersisted(true)
|
||||
}
|
||||
val dispatchResult = jobScheduler.schedule(jobInfo)
|
||||
if (dispatchResult != RESULT_SUCCESS) {
|
||||
log("Failed to schedule a check job for site: ${site.id}")
|
||||
} else {
|
||||
log("Check job successfully scheduled for site: ${site.id}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancelCheck(site: ServerModel) {
|
||||
check(site.id != 0) { "Cannot cancel scheduled checks for jobs with no ID." }
|
||||
log("Cancelling scheduled checks for site: ${site.id}")
|
||||
jobScheduler.cancel(site.id)
|
||||
}
|
||||
|
||||
override suspend fun performCheck(site: ServerModel): CheckResult {
|
||||
check(site.id != 0) { "Cannot schedule checks for jobs with no ID." }
|
||||
log("performCheck(${site.id}) - GET ${site.url}")
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(site.url)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
return try {
|
||||
val response = okHttpClient.newCall(request)
|
||||
.execute()
|
||||
if (response.isSuccessful || response.code() == 401) {
|
||||
log("performCheck(${site.id}) = Successful")
|
||||
CheckResult(
|
||||
model = site.copy(status = OK, reason = null),
|
||||
response = response
|
||||
)
|
||||
} else {
|
||||
log("performCheck(${site.id}) = Failure, HTTP code ${response.code()}")
|
||||
CheckResult(
|
||||
model = site.copy(
|
||||
status = ERROR,
|
||||
reason = "Response ${response.code()} - ${response.body()?.string() ?: "Unknown"}"
|
||||
),
|
||||
response = response
|
||||
)
|
||||
}
|
||||
} catch (timeoutEx: SocketTimeoutException) {
|
||||
log("performCheck(${site.id}) = Socket Timeout")
|
||||
CheckResult(
|
||||
model = site.copy(
|
||||
status = ERROR,
|
||||
reason = stringProvider.get(R.string.timeout)
|
||||
)
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
log("performCheck(${site.id}) = Error: ${ex.message}")
|
||||
CheckResult(model = site.copy(status = ERROR, reason = ex.message))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.engine.statuscheck
|
||||
|
||||
import android.app.job.JobInfo
|
||||
import android.app.job.JobService
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
|
||||
typealias JobInfoBuilder = JobInfo.Builder
|
||||
|
||||
fun jobInfo(
|
||||
context: Context,
|
||||
id: Int,
|
||||
target: Class<out JobService>,
|
||||
exec: JobInfoBuilder.() -> JobInfoBuilder
|
||||
): JobInfo {
|
||||
val component = ComponentName(context, target)
|
||||
val builder = JobInfo.Builder(id, component)
|
||||
exec(builder)
|
||||
return builder.build()
|
||||
}
|
6
engine/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<resources>
|
||||
|
||||
<string name="check_service_name">Nock Nock Status Service</string>
|
||||
<string name="timeout">Request timed out! Your server is probably down.</string>
|
||||
|
||||
</resources>
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
|
||||
|
|
1
notifications/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
35
notifications/build.gradle
Normal file
|
@ -0,0 +1,35 @@
|
|||
apply from: '../dependencies.gradle'
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion versions.minSdk
|
||||
targetSdkVersion versions.compileSdk
|
||||
versionCode versions.publishVersionCode
|
||||
versionName versions.publishVersion
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':data')
|
||||
implementation project(':utilities')
|
||||
|
||||
api 'androidx.appcompat:appcompat:' + versions.androidx
|
||||
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin
|
||||
api 'org.jetbrains.kotlinx:kotlinx-coroutines-core:' + versions.coroutines
|
||||
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:' + versions.coroutines
|
||||
|
||||
implementation 'com.google.dagger:dagger:' + versions.dagger
|
||||
kapt 'com.google.dagger:dagger-compiler:' + versions.dagger
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
apply from: '../spotless.gradle'
|
2
notifications/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<manifest
|
||||
package="com.afollestad.nocknock.notifications"/>
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.notifications
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.content.Context
|
||||
import android.os.Build.VERSION_CODES
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_DEFAULT
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
enum class Channel(
|
||||
val id: String,
|
||||
val title: Int,
|
||||
val description: Int,
|
||||
val importance: Int
|
||||
) {
|
||||
Statuses(
|
||||
id = "statuses",
|
||||
title = R.string.channel_server_status_title,
|
||||
description = R.string.channel_server_status_description,
|
||||
importance = IMPORTANCE_DEFAULT
|
||||
)
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
@RequiresApi(VERSION_CODES.O)
|
||||
fun Channel.toNotificationChannel(context: Context): NotificationChannel {
|
||||
val titleText = context.getString(this.title)
|
||||
val descriptionText = context.getString(this.description)
|
||||
return NotificationChannel(this.id, titleText, this.importance)
|
||||
.apply {
|
||||
description = descriptionText
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.notifications
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_CANCEL_CURRENT
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import androidx.core.app.NotificationCompat.DEFAULT_VIBRATE
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.notifications.Channel.Statuses
|
||||
import com.afollestad.nocknock.utilities.providers.BitmapProvider
|
||||
import com.afollestad.nocknock.utilities.providers.StringProvider
|
||||
import com.afollestad.nocknock.utilities.qualifiers.AppIconRes
|
||||
import com.afollestad.nocknock.utilities.util.hasOreo
|
||||
import javax.inject.Inject
|
||||
|
||||
const val STATUS_NOTIFICATION_ID = 3456
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
interface NockNotificationManager {
|
||||
|
||||
fun setIsAppOpen(open: Boolean)
|
||||
|
||||
fun createChannels()
|
||||
|
||||
fun postStatusNotification(model: ServerModel)
|
||||
|
||||
fun cancelStatusNotification(model: ServerModel)
|
||||
|
||||
fun cancelStatusNotifications()
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
class RealNockNotificationManager @Inject constructor(
|
||||
private val app: Application,
|
||||
@AppIconRes private val appIconRes: Int,
|
||||
private val stockManager: NotificationManager,
|
||||
private val bitmapProvider: BitmapProvider,
|
||||
private val stringProvider: StringProvider
|
||||
) : NockNotificationManager {
|
||||
companion object {
|
||||
private const val BASE_REQUEST_CODE = 44
|
||||
|
||||
const val KEY_MODEL = "model"
|
||||
}
|
||||
|
||||
private var isAppOpen = false
|
||||
|
||||
override fun setIsAppOpen(open: Boolean) {
|
||||
this.isAppOpen = open
|
||||
}
|
||||
|
||||
override fun createChannels() {
|
||||
Channel.values()
|
||||
.forEach(this::createChannel)
|
||||
}
|
||||
|
||||
override fun postStatusNotification(model: ServerModel) {
|
||||
if (isAppOpen) {
|
||||
// Don't show notifications while the app is open
|
||||
return
|
||||
}
|
||||
|
||||
val viewSiteActivityCls =
|
||||
Class.forName("com.afollestad.nocknock.ui.ViewSiteActivity")
|
||||
val openIntent = Intent(app, viewSiteActivityCls).apply {
|
||||
putExtra(KEY_MODEL, model)
|
||||
addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
val openPendingIntent = PendingIntent.getBroadcast(
|
||||
app,
|
||||
BASE_REQUEST_CODE + model.id,
|
||||
openIntent,
|
||||
FLAG_CANCEL_CURRENT
|
||||
)
|
||||
|
||||
val newNotification = notification(app, Statuses) {
|
||||
setContentTitle(model.name)
|
||||
setContentText(stringProvider.get(R.string.something_wrong))
|
||||
setContentIntent(openPendingIntent)
|
||||
setSmallIcon(R.drawable.ic_notification)
|
||||
setLargeIcon(bitmapProvider.get(appIconRes))
|
||||
setAutoCancel(true)
|
||||
setDefaults(DEFAULT_VIBRATE)
|
||||
}
|
||||
|
||||
stockManager.notify(model.url, STATUS_NOTIFICATION_ID, newNotification)
|
||||
}
|
||||
|
||||
override fun cancelStatusNotification(model: ServerModel) {
|
||||
stockManager.cancel(BASE_REQUEST_CODE + model.id)
|
||||
}
|
||||
|
||||
override fun cancelStatusNotifications() {
|
||||
stockManager.cancelAll()
|
||||
}
|
||||
|
||||
private fun createChannel(channel: Channel) {
|
||||
if (!hasOreo()) {
|
||||
return
|
||||
}
|
||||
val notificationChannel = channel.toNotificationChannel(app)
|
||||
stockManager.createNotificationChannel(notificationChannel)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.notifications
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
typealias NotificationBuilder = NotificationCompat.Builder
|
||||
typealias NotificationConstructor = NotificationBuilder.() -> Unit
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
fun notification(
|
||||
context: Context,
|
||||
channel: Channel,
|
||||
builder: NotificationConstructor
|
||||
): Notification {
|
||||
val newNotification = NotificationCompat.Builder(context, channel.id)
|
||||
builder(newNotification)
|
||||
return newNotification.build()
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.notifications
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** @author Aidan Follestad (afollestad) */
|
||||
@Module
|
||||
abstract class NotificationsModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideNockNotificationManager(
|
||||
notificationManager: RealNockNotificationManager
|
||||
): NockNotificationManager
|
||||
}
|
Before Width: | Height: | Size: 998 B After Width: | Height: | Size: 998 B |
Before Width: | Height: | Size: 661 B After Width: | Height: | Size: 661 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |