diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md
deleted file mode 100644
index 2d7d09f..0000000
--- a/.github/ISSUE_TEMPLATE/Bug_report.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-name: Bug report
-about: Something is crashing or not working as intended
-
----
-
-*Please consider making a Pull Request if you are capable of doing so.*
-
-**App Version:**
-
-x.x.x
-
-**Affected Device(s):**
-
-Google Pixel 3 XL with Android 9.0
-
-**Describe the Bug:**
-
-A clear description of what is the bug is.
-
-**To Reproduce:**
-1.
-2.
-3.
-
-**Expected Behavior:**
-
-A clear description of what you expected to happen.
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md
deleted file mode 100644
index 77310ae..0000000
--- a/.github/ISSUE_TEMPLATE/Feature_request.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for this project
-
----
-
-*Please consider making a Pull Request if you are capable of doing so.*
-
-**Description what you'd like to happen:**
-
-A clear description if the feature or behavior you'd like implemented.
-
-**Describe alternatives you've considered:**
-
-A clear description of any alternative solutions you've considered.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
deleted file mode 100644
index 6307e10..0000000
--- a/.github/pull_request_template.md
+++ /dev/null
@@ -1,8 +0,0 @@
-### Guidelines
-
-1. You must run the `spotlessApply` task before committing, either through Android Studio or with `./gradlew spotlessApply`.
-2. A PR should be focused and contained. If you are changing multiple unrelated things, they should be in separate PRs.
-3. A PR should fix a bug or solve a problem - something that only you would use is not necessarily something that should be published.
-4. Give your PR a detailed title and description - look over your code one last time before actually creating the PR. Give it a self-review.
-
-**If you do not follow the guidelines, your PR will be rejected.**
diff --git a/.gitignore b/.gitignore
index 454e51a..161128f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -180,6 +180,4 @@ gradle-app.setting
.gradletasknamecache
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
-# gradle/wrapper/gradle-wrapper.properties
-
-app/google-services.json
\ No newline at end of file
+# gradle/wrapper/gradle-wrapper.properties
\ No newline at end of file
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..f519041
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+nock-nock
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..96cc43e
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 0000000..e7bedf3
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..97626ba
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 50f0406..fbb6828 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,49 +1,46 @@
-
-
-
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
index 23f5d23..56aca0f 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -3,12 +3,7 @@
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..70085e5
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,22 @@
+language: android
+jdk: oraclejdk8
+android:
+ components:
+ - tools
+ - platform-tools
+ - build-tools-24.0.1
+ - android-24
+ - extra-android-support
+ - extra-android-m2repository
+ - extra-google-m2repository
+
+ # Additional components
+ #- extra-google-google_play_services
+ #- addon-google_apis-google-19
+
+ # Specify at least one system image, if you need to run emulator(s) during your tests
+ #- sys-img-armeabi-v7a-android-19
+ #- sys-img-x86-android-17
+
+ licenses:
+ - '.+'
\ No newline at end of file
diff --git a/NockNock-0.1.3.0.apk b/NockNock-0.1.3.0.apk
new file mode 100644
index 0000000..c4d3367
Binary files /dev/null and b/NockNock-0.1.3.0.apk differ
diff --git a/README.md b/README.md
index 84ee16c..a15f38e 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,9 @@
## Nock Nock
+[](https://travis-ci.org/afollestad/nock-nock)
[](https://www.apache.org/licenses/LICENSE-2.0.html)
-
+
Nock Nock is a simple app which allows you to monitor your websites for maximum uptime.
@@ -10,4 +11,4 @@ The app will automatically knock on the door of your websites (or web servers) o
to make sure they are up and responding successfully. If something is wrong, you get a notification telling you so.
-
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index d6b315e..23e53a1 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,79 +1,43 @@
-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 24
+ buildToolsVersion "24.0.1"
- defaultConfig {
- applicationId "com.afollestad.nocknock"
- minSdkVersion versions.minSdk
- targetSdkVersion versions.compileSdk
- versionCode versions.publishVersionCode
- versionName versions.publishVersion
- }
+ defaultConfig {
+ applicationId "com.afollestad.nocknock"
+ minSdkVersion 21
+ targetSdkVersion 24
+ versionCode 13
+ versionName "0.1.3.0"
- compileOptions {
- sourceCompatibility 1.8
- targetCompatibility 1.8
- }
+ lintOptions {
+ abortOnError false
+ }
+ jackOptions {
+ enabled true
+ }
+ }
- packagingOptions {
- exclude 'META-INF/atomicfu.kotlin_module'
- }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
}
dependencies {
- implementation project(':common')
- implementation project(':engine')
- implementation project(':data')
- implementation project(':notifications')
- implementation project(':viewcomponents')
-
- // Google/AppCompat
- implementation 'androidx.appcompat:appcompat:' + versions.androidxCore
- implementation 'androidx.recyclerview:recyclerview:' + versions.androidxRecyclerView
- implementation 'com.google.android.material:material:' + versions.googleMaterial
- implementation 'androidx.browser:browser:' + versions.androidxBrowser
- implementation 'com.google.firebase:firebase-core:' + versions.firebaseCore
-
- // Lifecycle
- kapt 'androidx.lifecycle:lifecycle-compiler:' + versions.lifecycle
-
- // Kotlin
- implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin
-
- // JOIN
- implementation 'org.koin:koin-android:' + versions.koin
- implementation 'org.koin:koin-androidx-scope:' + versions.koin
- implementation 'org.koin:koin-androidx-viewmodel:' + versions.koin
-
- // afollestad
- implementation 'com.afollestad.material-dialogs:core:' + versions.materialDialogs
-
- // Debugging
- implementation 'com.jakewharton.timber:timber:' + versions.timber
- implementation("com.crashlytics.sdk.android:crashlytics:${versions.fabric}") {
- transitive = true
- }
-
- // Testing
- testImplementation 'junit:junit:' + versions.junit
- testImplementation 'org.mockito:mockito-core:' + versions.mockito
- testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:' + versions.mockitoKotlin
- testImplementation 'com.google.truth:truth:' + versions.truth
- testImplementation 'androidx.arch.core:core-testing:' + versions.archTesting
-
- // UI testing
- androidTestImplementation 'androidx.test:runner:' + versions.androidxTestRunner
- androidTestImplementation 'androidx.test:rules:' + versions.androidxTestRunner
-}
-
-apply from: '../spotless.gradle'
-apply from: '../mock/mock.gradle'
-
-apply plugin: "io.fabric"
-apply plugin: 'com.google.gms.google-services'
\ No newline at end of file
+ compile 'com.android.support:appcompat-v7:24.2.0'
+ compile 'com.android.support:design:24.2.0'
+ compile 'com.afollestad.material-dialogs:core:0.9.0.1'
+ compile 'com.afollestad.material-dialogs:commons:0.9.0.1'
+ compile 'com.afollestad:bridge:3.2.5'
+ compile 'com.afollestad:inquiry:3.2.1'
+ compile files('libs/rhino-1.7.7.1.jar')
+}
\ No newline at end of file
diff --git a/app/libs/rhino-1.7.7.1.jar b/app/libs/rhino-1.7.7.1.jar
new file mode 100644
index 0000000..a8b9417
Binary files /dev/null and b/app/libs/rhino-1.7.7.1.jar differ
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..666743b
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,17 @@
+# 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 *;
+#}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 332578e..d6a9d32 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,57 +3,60 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.afollestad.nocknock">
-
-
-
+
+
+
-
+
-
-
-
+
+
+
-
-
-
+
+
+
-
+
-
+
-
+
-
-
-
-
-
+
+
+
+
+
-
+
+
+
+
+
+
-
+
-
+
\ No newline at end of file
diff --git a/app/src/main/java/com/afollestad/nocknock/AppExt.kt b/app/src/main/java/com/afollestad/nocknock/AppExt.kt
deleted file mode 100644
index 828e01f..0000000
--- a/app/src/main/java/com/afollestad/nocknock/AppExt.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock
-
-import android.app.Activity
-import android.app.Application
-import android.app.Application.ActivityLifecycleCallbacks
-import android.content.ActivityNotFoundException
-import android.content.Intent
-import android.os.Bundle
-import androidx.browser.customtabs.CustomTabsIntent
-import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
-import androidx.core.text.HtmlCompat.fromHtml
-import com.afollestad.materialdialogs.utils.MDUtil.resolveColor
-import com.afollestad.nocknock.utilities.ext.toUri
-import com.afollestad.nocknock.utilities.ui.toast
-
-typealias ActivityLifeChange = (activity: Activity, resumed: Boolean) -> Unit
-
-/** @author Aidan Follestad (@afollestad) */
-fun Application.onActivityLifeChange(cb: ActivityLifeChange) {
- registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
- override fun onActivitySaveInstanceState(
- activity: Activity?,
- outState: Bundle?
- ) = Unit
-
- override fun onActivityPaused(activity: Activity) = cb(activity, false)
-
- override fun onActivityResumed(activity: Activity) = cb(activity, true)
-
- override fun onActivityStarted(activity: Activity) = Unit
-
- override fun onActivityDestroyed(activity: Activity) = Unit
-
- override fun onActivityStopped(activity: Activity) = Unit
-
- override fun onActivityCreated(
- activity: Activity?,
- savedInstanceState: Bundle?
- ) = Unit
- })
-}
-
-fun String.toHtml() = fromHtml(this, FROM_HTML_MODE_LEGACY)
-
-fun Activity.viewUrl(url: String) {
- val customTabsIntent = CustomTabsIntent.Builder()
- .apply {
- setToolbarColor(resolveColor(this@viewUrl, attr = R.attr.colorPrimary))
- }
- .build()
- try {
- customTabsIntent.launchUrl(this, url.toUri())
- } catch (_: ActivityNotFoundException) {
- toast(R.string.install_web_browser)
- }
-}
-
-fun Activity.viewUrlWithApp(
- url: String,
- pkg: String
-) {
- val intent = Intent(Intent.ACTION_VIEW).apply {
- data = url.toUri()
- }
- val resInfo = packageManager.queryIntentActivities(intent, 0)
- for (info in resInfo) {
- if (info.activityInfo.packageName.toLowerCase().contains(pkg) ||
- info.activityInfo.name.toLowerCase().contains(pkg)
- ) {
- startActivity(intent.apply {
- setPackage(info.activityInfo.packageName)
- })
- return
- }
- }
- viewUrl(url)
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/NockNockApp.kt b/app/src/main/java/com/afollestad/nocknock/NockNockApp.kt
deleted file mode 100644
index 3c29301..0000000
--- a/app/src/main/java/com/afollestad/nocknock/NockNockApp.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-@file:Suppress("unused")
-
-package com.afollestad.nocknock
-
-import android.app.Application
-import com.afollestad.nocknock.BuildConfig.DEBUG
-import com.afollestad.nocknock.engine.engineModule
-import com.afollestad.nocknock.koin.mainModule
-import com.afollestad.nocknock.koin.prefModule
-import com.afollestad.nocknock.koin.viewModelModule
-import com.afollestad.nocknock.logging.FabricTree
-import com.afollestad.nocknock.notifications.NockNotificationManager
-import com.afollestad.nocknock.notifications.notificationsModule
-import com.afollestad.nocknock.utilities.commonModule
-import com.crashlytics.android.Crashlytics
-import io.fabric.sdk.android.Fabric
-import org.koin.android.ext.android.inject
-import org.koin.android.ext.android.startKoin
-import timber.log.Timber
-import timber.log.Timber.DebugTree
-import timber.log.Timber.d as log
-
-/** @author Aidan Follestad (@afollestad) */
-class NockNockApp : Application() {
-
- private var resumedActivities: Int = 0
-
- override fun onCreate() {
- super.onCreate()
-
- if (DEBUG) {
- Timber.plant(DebugTree())
- }
-
- Timber.plant(FabricTree())
- Fabric.with(this, Crashlytics())
-
- val modules = listOf(
- prefModule,
- mainModule,
- engineModule,
- commonModule,
- notificationsModule,
- viewModelModule
- )
- startKoin(
- androidContext = this,
- modules = modules
- )
-
- val nockNotificationManager by inject()
- onActivityLifeChange { activity, resumed ->
- if (resumed) {
- resumedActivities++
- log("Activity resumed: $activity, resumedActivities = $resumedActivities")
- } else {
- resumedActivities--
- log("Activity paused: $activity, resumedActivities = $resumedActivities")
- }
- check(resumedActivities >= 0) { "resumedActivities can't go below 0." }
- nockNotificationManager.setIsAppOpen(resumedActivities > 0)
- }
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/adapter/ServerAdapter.java b/app/src/main/java/com/afollestad/nocknock/adapter/ServerAdapter.java
new file mode 100644
index 0000000..b4da56b
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/adapter/ServerAdapter.java
@@ -0,0 +1,171 @@
+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 {
+
+ private final Object LOCK = new Object();
+ private ArrayList mServers;
+ private ClickListener mListener;
+
+ public interface ClickListener {
+ void onSiteSelected(int index, ServerModel model, boolean longClick);
+ }
+
+ public void performClick(int index, boolean longClick) {
+ if (mListener != null) {
+ mListener.onSiteSelected(index, mServers.get(index), longClick);
+ }
+ }
+
+ public ServerAdapter(ClickListener listener) {
+ mListener = listener;
+ mServers = new ArrayList<>(2);
+ }
+
+ public void add(ServerModel model) {
+ mServers.add(model);
+ notifyItemInserted(mServers.size() - 1);
+ }
+
+ public void update(int index, ServerModel model) {
+ mServers.set(index, model);
+ notifyItemChanged(index);
+ }
+
+ public void update(ServerModel model) {
+ synchronized (LOCK) {
+ for (int i = 0; i < mServers.size(); i++) {
+ if (mServers.get(i).id == model.id) {
+ update(i, model);
+ break;
+ }
+ }
+ }
+ }
+
+ public void remove(int index) {
+ mServers.remove(index);
+ notifyItemRemoved(index);
+ }
+
+ public void remove(ServerModel model) {
+ synchronized (LOCK) {
+ for (int i = 0; i < mServers.size(); i++) {
+ if (mServers.get(i).id == model.id) {
+ remove(i);
+ break;
+ }
+ }
+ }
+ }
+
+ public void set(ServerModel[] models) {
+ if (models == null || models.length == 0) {
+ mServers.clear();
+ return;
+ }
+ mServers = new ArrayList<>(models.length);
+ Collections.addAll(mServers, models);
+ notifyDataSetChanged();
+ }
+
+ public void clear() {
+ mServers.clear();
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public ServerAdapter.ServerVH onCreateViewHolder(ViewGroup parent, int viewType) {
+ final View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_server, parent, false);
+ return new ServerVH(v, this);
+ }
+
+ @Override
+ public void onBindViewHolder(ServerAdapter.ServerVH holder, int position) {
+ final ServerModel model = mServers.get(position);
+
+ holder.textName.setText(model.name);
+ holder.textUrl.setText(model.url);
+ holder.iconStatus.setStatus(model.status);
+
+ switch (model.status) {
+ case ServerStatus.OK:
+ holder.textStatus.setText(R.string.everything_checks_out);
+ break;
+ case ServerStatus.WAITING:
+ holder.textStatus.setText(R.string.waiting);
+ break;
+ case ServerStatus.CHECKING:
+ holder.textStatus.setText(R.string.checking_status);
+ break;
+ case ServerStatus.ERROR:
+ holder.textStatus.setText(model.reason);
+ break;
+ }
+
+ if (model.checkInterval <= 0) {
+ holder.textInterval.setText("");
+ } else {
+ final long now = System.currentTimeMillis();
+ final long nextCheck = model.lastCheck + model.checkInterval;
+ final long difference = nextCheck - now;
+ holder.textInterval.setText(TimeUtil.str(difference));
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return mServers.size();
+ }
+
+ public static class ServerVH extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
+
+ final StatusImageView iconStatus;
+ final TextView textName;
+ final TextView textInterval;
+ final TextView textUrl;
+ final TextView textStatus;
+ final ServerAdapter adapter;
+
+ public ServerVH(View itemView, ServerAdapter adapter) {
+ super(itemView);
+ iconStatus = (StatusImageView) itemView.findViewById(R.id.iconStatus);
+ textName = (TextView) itemView.findViewById(R.id.textName);
+ textInterval = (TextView) itemView.findViewById(R.id.textInterval);
+ textUrl = (TextView) itemView.findViewById(R.id.textUrl);
+ textStatus = (TextView) itemView.findViewById(R.id.textStatus);
+ this.adapter = adapter;
+
+ itemView.setOnClickListener(this);
+ itemView.setOnLongClickListener(this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ adapter.performClick(getAdapterPosition(), false);
+ }
+
+ @Override
+ public boolean onLongClick(View view) {
+ adapter.performClick(getAdapterPosition(), true);
+ return false;
+ }
+ }
+}
diff --git a/app/src/main/java/com/afollestad/nocknock/adapter/SiteAdapter.kt b/app/src/main/java/com/afollestad/nocknock/adapter/SiteAdapter.kt
deleted file mode 100644
index e1b034c..0000000
--- a/app/src/main/java/com/afollestad/nocknock/adapter/SiteAdapter.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.adapter
-
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.recyclerview.widget.DiffUtil.calculateDiff
-import androidx.recyclerview.widget.RecyclerView
-import com.afollestad.nocknock.R
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.data.model.Status.WAITING
-import com.afollestad.nocknock.data.model.isPending
-import com.afollestad.nocknock.data.model.textRes
-import com.afollestad.nocknock.utilities.ui.onDebouncedClick
-import kotlinx.android.synthetic.main.list_item_server.view.iconStatus
-import kotlinx.android.synthetic.main.list_item_server.view.textInterval
-import kotlinx.android.synthetic.main.list_item_server.view.textName
-import kotlinx.android.synthetic.main.list_item_server.view.textStatus
-import kotlinx.android.synthetic.main.list_item_server.view.textUrl
-
-typealias Listener = (model: Site, longClick: Boolean) -> Unit
-
-/** @author Aidan Follestad (@afollestad) */
-class SiteViewHolder constructor(
- itemView: View,
- private val adapter: SiteAdapter
-) : RecyclerView.ViewHolder(itemView), View.OnLongClickListener {
-
- init {
- itemView.onDebouncedClick {
- adapter.performClick(adapterPosition, false)
- }
- itemView.setOnLongClickListener(this)
- }
-
- fun bind(model: Site) {
- requireNotNull(model.settings) { "Settings must be populated." }
-
- itemView.textName.text = model.name
- itemView.textUrl.text = model.url
-
- val lastResult = model.lastResult
- if (lastResult != null) {
- itemView.iconStatus.setStatus(lastResult.status)
- val statusText = lastResult.status.textRes()
- if (statusText == 0) {
- itemView.textStatus.text = lastResult.reason
- } else {
- itemView.textStatus.setText(statusText)
- }
- } else {
- itemView.iconStatus.setStatus(WAITING)
- itemView.textStatus.setText(R.string.none)
- }
-
- val res = itemView.resources
- when {
- model.settings?.disabled == true -> {
- itemView.textInterval.setText(R.string.checks_disabled)
- }
- model.lastResult?.status.isPending() -> {
- itemView.textInterval.text = res.getString(
- R.string.next_check_x,
- res.getString(R.string.now)
- )
- }
- else -> {
- itemView.textInterval.text = res.getString(
- R.string.next_check_x,
- model.intervalText()
- )
- }
- }
- }
-
- override fun onLongClick(view: View): Boolean {
- adapter.performClick(adapterPosition, true)
- return false
- }
-}
-
-/** @author Aidan Follestad (@afollestad) */
-class SiteAdapter(private val listener: Listener) : RecyclerView.Adapter() {
-
- private var models = mutableListOf()
-
- internal fun performClick(
- index: Int,
- longClick: Boolean
- ) = listener.invoke(models[index], longClick)
-
- fun set(newModels: List) {
- val formerModels = this.models
- this.models = newModels.toMutableList()
- val diffResult = calculateDiff(SiteDiffCallback(formerModels, this.models))
- diffResult.dispatchUpdatesTo(this)
- }
-
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int
- ): SiteViewHolder {
- val v = LayoutInflater.from(parent.context)
- .inflate(R.layout.list_item_server, parent, false)
- return SiteViewHolder(v, this)
- }
-
- override fun onBindViewHolder(
- holder: SiteViewHolder,
- position: Int
- ) {
- val model = models[position]
- holder.bind(model)
- }
-
- override fun getItemCount() = models.size
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/adapter/SiteDiffCallback.kt b/app/src/main/java/com/afollestad/nocknock/adapter/SiteDiffCallback.kt
deleted file mode 100644
index de8b7ca..0000000
--- a/app/src/main/java/com/afollestad/nocknock/adapter/SiteDiffCallback.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.adapter
-
-import androidx.recyclerview.widget.DiffUtil
-import com.afollestad.nocknock.data.model.Site
-
-/** @author Aidan Follestad (@afollestad) */
-class SiteDiffCallback(
- private val oldItems: List,
- private val newItems: List
-) : DiffUtil.Callback() {
-
- override fun getOldListSize() = oldItems.size
-
- override fun getNewListSize() = newItems.size
-
- override fun areItemsTheSame(
- oldItemPosition: Int,
- newItemPosition: Int
- ) = oldItems[oldItemPosition].id == newItems[newItemPosition].id
-
- override fun areContentsTheSame(
- oldItemPosition: Int,
- newItemPosition: Int
- ) = oldItems[oldItemPosition] == newItems[newItemPosition]
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/adapter/TagAdapter.kt b/app/src/main/java/com/afollestad/nocknock/adapter/TagAdapter.kt
deleted file mode 100644
index 7bf22e5..0000000
--- a/app/src/main/java/com/afollestad/nocknock/adapter/TagAdapter.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.adapter
-
-import android.graphics.Color.WHITE
-import android.view.LayoutInflater
-import android.view.View
-import android.view.View.OnClickListener
-import android.view.ViewGroup
-import androidx.core.content.ContextCompat
-import androidx.recyclerview.widget.RecyclerView
-import androidx.recyclerview.widget.RecyclerView.ViewHolder
-import com.afollestad.nocknock.R
-import com.afollestad.nocknock.adapter.TagAdapter.TagViewHolder
-import kotlinx.android.synthetic.main.list_item_tag.view.chip
-
-typealias TagsListener = (tags: List) -> Unit
-
-/** @author Aidan Follestad (@afollestad) */
-class TagAdapter(
- private val listener: TagsListener
-) : RecyclerView.Adapter() {
-
- private val tags = mutableListOf()
- private val checked = mutableListOf()
-
- fun set(tags: List) {
- this.tags.run {
- clear()
- addAll(tags)
- }
- notifyDataSetChanged()
- }
-
- fun toggleChecked(index: Int) {
- if (checked.contains(index)) {
- checked.remove(index)
- } else {
- checked.add(index)
- }
- notifyItemChanged(index)
- listener.invoke(getCheckedTags())
- }
-
- private fun getCheckedTags(): List {
- return mutableListOf().apply {
- checked.forEach { index -> add(tags[index]) }
- }
- }
-
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int
- ): TagViewHolder {
- val view = LayoutInflater.from(parent.context)
- .inflate(R.layout.list_item_tag, parent, false)
- return TagViewHolder(view, this)
- }
-
- override fun getItemCount() = tags.size
-
- override fun onBindViewHolder(
- holder: TagViewHolder,
- position: Int
- ) {
- holder.bind(tags[position], checked.contains(position))
- }
-
- /** @author Aidan Follestad (@afollestad) */
- class TagViewHolder(
- itemView: View,
- private val adapter: TagAdapter
- ) : ViewHolder(itemView), OnClickListener {
-
- override fun onClick(v: View) = adapter.toggleChecked(adapterPosition)
-
- init {
- itemView.setOnClickListener(this)
- }
-
- fun bind(
- name: String,
- checked: Boolean
- ) = itemView.chip.run {
- text = name
- setTextColor(
- if (checked) {
- WHITE
- } else {
- ContextCompat.getColor(itemView.context, R.color.unchecked_chip_text)
- }
- )
- setBackgroundResource(
- if (checked) {
- R.drawable.checked_chip_selector
- } else {
- R.drawable.unchecked_chip_selector
- }
- )
- }
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/api/ServerModel.java b/app/src/main/java/com/afollestad/nocknock/api/ServerModel.java
new file mode 100644
index 0000000..1e3b6df
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/api/ServerModel.java
@@ -0,0 +1,36 @@
+package com.afollestad.nocknock.api;
+
+import com.afollestad.inquiry.annotations.Column;
+
+import java.io.Serializable;
+
+/**
+ * @author Aidan Follestad (afollestad)
+ */
+public class ServerModel implements Serializable {
+
+ public ServerModel() {
+ }
+
+ @Column(name = "_id", primaryKey = true, notNull = true, autoIncrement = true)
+ public long id;
+ @Column
+ public String name;
+ @Column
+ public String url;
+ @Column
+ @ServerStatus.Enum
+ public int status;
+ @Column
+ public long checkInterval;
+ @Column
+ public long lastCheck;
+ @Column
+ public String reason;
+
+ @Column
+ @ValidationMode.Enum
+ public int validationMode;
+ @Column
+ public String validationContent;
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/afollestad/nocknock/api/ServerStatus.java b/app/src/main/java/com/afollestad/nocknock/api/ServerStatus.java
new file mode 100644
index 0000000..f7f98fe
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/api/ServerStatus.java
@@ -0,0 +1,21 @@
+package com.afollestad.nocknock.api;
+
+import android.support.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * @author Aidan Follestad (afollestad)
+ */
+public final class ServerStatus {
+
+ public final static int OK = 1;
+ public final static int WAITING = 2;
+ public final static int CHECKING = 3;
+ public final static int ERROR = 4;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({OK, WAITING, CHECKING, ERROR})
+ public @interface Enum {}
+}
diff --git a/app/src/main/java/com/afollestad/nocknock/api/ValidationMode.java b/app/src/main/java/com/afollestad/nocknock/api/ValidationMode.java
new file mode 100644
index 0000000..2606635
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/api/ValidationMode.java
@@ -0,0 +1,21 @@
+package com.afollestad.nocknock.api;
+
+import android.support.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * @author Aidan Follestad (afollestad)
+ */
+public final class ValidationMode {
+
+ public final static int STATUS_CODE = 1;
+ public final static int TERM_SEARCH = 2;
+ public final static int JAVASCRIPT = 3;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({STATUS_CODE, TERM_SEARCH, JAVASCRIPT})
+ public @interface Enum {
+ }
+}
diff --git a/app/src/main/java/com/afollestad/nocknock/broadcasts/StatusUpdateIntentReceiver.kt b/app/src/main/java/com/afollestad/nocknock/broadcasts/StatusUpdateIntentReceiver.kt
deleted file mode 100644
index c5567c5..0000000
--- a/app/src/main/java/com/afollestad/nocknock/broadcasts/StatusUpdateIntentReceiver.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.broadcasts
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import androidx.lifecycle.Lifecycle.Event.ON_DESTROY
-import androidx.lifecycle.Lifecycle.Event.ON_PAUSE
-import androidx.lifecycle.Lifecycle.Event.ON_RESUME
-import androidx.lifecycle.LifecycleObserver
-import androidx.lifecycle.OnLifecycleEvent
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.engine.validation.ValidationJob.Companion.ACTION_STATUS_UPDATE
-import com.afollestad.nocknock.engine.validation.ValidationJob.Companion.KEY_UPDATE_MODEL
-import com.afollestad.nocknock.utilities.providers.IntentProvider
-
-typealias SiteCallback = (Site) -> Unit
-
-/** @author Aidan Follestad (@afollestad) */
-class StatusUpdateIntentReceiver(
- private val context: Context,
- private val intentProvider: IntentProvider,
- private var callback: SiteCallback?
-) : LifecycleObserver {
-
- internal val intentReceiver = object : BroadcastReceiver() {
- override fun onReceive(
- context: Context,
- intent: Intent
- ) {
- if (intent.action == ACTION_STATUS_UPDATE) {
- val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? Site
- ?: return
- callback?.invoke(model)
- }
- }
- }
-
- @OnLifecycleEvent(ON_RESUME)
- fun onResume() {
- val filter = intentProvider.createFilter(ACTION_STATUS_UPDATE)
- context.registerReceiver(intentReceiver, filter)
- }
-
- @OnLifecycleEvent(ON_PAUSE)
- fun onPause() {
- context.unregisterReceiver(intentReceiver)
- }
-
- @OnLifecycleEvent(ON_DESTROY)
- fun onDestroy() {
- callback = null
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/dialogs/AboutDialog.java b/app/src/main/java/com/afollestad/nocknock/dialogs/AboutDialog.java
new file mode 100644
index 0000000..3eeed80
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/dialogs/AboutDialog.java
@@ -0,0 +1,33 @@
+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();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/afollestad/nocknock/dialogs/AboutDialog.kt b/app/src/main/java/com/afollestad/nocknock/dialogs/AboutDialog.kt
deleted file mode 100644
index f0c152f..0000000
--- a/app/src/main/java/com/afollestad/nocknock/dialogs/AboutDialog.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.dialogs
-
-import android.app.Dialog
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import androidx.fragment.app.DialogFragment
-import com.afollestad.materialdialogs.MaterialDialog
-import com.afollestad.nocknock.BuildConfig
-import com.afollestad.nocknock.R
-
-/** @author Aidan Follestad (@afollestad) */
-class AboutDialog : DialogFragment() {
- companion object {
- private const val TAG = "[ABOUT_DIALOG]"
-
- fun show(context: AppCompatActivity) {
- val dialog = AboutDialog()
- dialog.show(context.supportFragmentManager, TAG)
- }
- }
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val context = activity ?: throw IllegalStateException("Oh no!")
- return MaterialDialog(context)
- .title(text = getString(R.string.app_name_x, BuildConfig.VERSION_NAME))
- .positiveButton(R.string.dismiss)
- .message(R.string.about_body, html = true, lineHeightMultiplier = 1.4f)
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/koin/MainModule.kt b/app/src/main/java/com/afollestad/nocknock/koin/MainModule.kt
deleted file mode 100644
index 07f6410..0000000
--- a/app/src/main/java/com/afollestad/nocknock/koin/MainModule.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.koin
-
-import android.app.Application
-import android.app.NotificationManager
-import android.app.job.JobScheduler
-import android.content.Context.JOB_SCHEDULER_SERVICE
-import android.content.Context.NOTIFICATION_SERVICE
-import androidx.room.Room.databaseBuilder
-import com.afollestad.nocknock.data.AppDatabase
-import com.afollestad.nocknock.data.Database1to2Migration
-import com.afollestad.nocknock.data.Database2to3Migration
-import com.afollestad.nocknock.data.Database3to4Migration
-import com.afollestad.nocknock.data.Database4to5Migration
-import com.afollestad.nocknock.notifications.Qualifiers.MAIN_ACTIVITY_CLASS
-import com.afollestad.nocknock.ui.main.MainActivity
-import com.afollestad.nocknock.utilities.ext.systemService
-import okhttp3.OkHttpClient
-import org.koin.dsl.module.module
-
-val mainActivityCls = MainActivity::class.java
-
-/** @author Aidan Follestad (@afollestad) */
-val mainModule = module {
-
- single(name = MAIN_ACTIVITY_CLASS) { mainActivityCls }
-
- single {
- databaseBuilder(get(), AppDatabase::class.java, "NockNock.db")
- .addMigrations(
- Database1to2Migration(),
- Database2to3Migration(),
- Database3to4Migration(),
- Database4to5Migration()
- )
- .build()
- }
-
- single {
- OkHttpClient.Builder()
- .addNetworkInterceptor { chain ->
- val request = chain.request()
- .newBuilder()
- .addHeader("User-Agent", "com.afollestad.nocknock")
- .build()
- chain.proceed(request)
- }
- .build()
- }
-
- single {
- get().systemService(JOB_SCHEDULER_SERVICE)
- }
-
- single {
- get().systemService(NOTIFICATION_SERVICE)
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/koin/MainPrefModule.kt b/app/src/main/java/com/afollestad/nocknock/koin/MainPrefModule.kt
deleted file mode 100644
index 654b76d..0000000
--- a/app/src/main/java/com/afollestad/nocknock/koin/MainPrefModule.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.koin
-
-import com.afollestad.rxkprefs.RxkPrefs
-import com.afollestad.rxkprefs.rxkPrefs
-import org.koin.dsl.module.module
-
-const val PREF_DARK_MODE = "dark_mode"
-
-/** @author Aidan Follestad (@afollestad) */
-val prefModule = module {
-
- single { rxkPrefs(get(), "settings") }
-
- factory(name = PREF_DARK_MODE) {
- get().boolean(PREF_DARK_MODE, false)
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/koin/ViewModelModule.kt b/app/src/main/java/com/afollestad/nocknock/koin/ViewModelModule.kt
deleted file mode 100644
index ab8cd79..0000000
--- a/app/src/main/java/com/afollestad/nocknock/koin/ViewModelModule.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.koin
-
-import com.afollestad.nocknock.ui.addsite.AddSiteViewModel
-import com.afollestad.nocknock.ui.main.MainViewModel
-import com.afollestad.nocknock.ui.viewsite.ViewSiteViewModel
-import com.afollestad.nocknock.utilities.Qualifiers.IO_DISPATCHER
-import com.afollestad.nocknock.utilities.Qualifiers.MAIN_DISPATCHER
-import org.koin.androidx.viewmodel.ext.koin.viewModel
-import org.koin.dsl.module.module
-
-/** @author Aidan Follestad (@afollestad) */
-val viewModelModule = module {
-
- viewModel {
- MainViewModel(
- get(),
- get(),
- get(),
- get(name = MAIN_DISPATCHER),
- get(name = IO_DISPATCHER)
- )
- }
-
- viewModel {
- AddSiteViewModel(
- get(),
- get(),
- get(name = MAIN_DISPATCHER),
- get(name = IO_DISPATCHER)
- )
- }
-
- viewModel {
- ViewSiteViewModel(
- get(),
- get(),
- get(),
- get(),
- get(name = MAIN_DISPATCHER),
- get(name = IO_DISPATCHER)
- )
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/logging/FabricTree.kt b/app/src/main/java/com/afollestad/nocknock/logging/FabricTree.kt
deleted file mode 100644
index 10b97fb..0000000
--- a/app/src/main/java/com/afollestad/nocknock/logging/FabricTree.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.logging
-
-import com.crashlytics.android.Crashlytics
-import timber.log.Timber
-
-/** @author Aidan Follestad (@afollestad) */
-class FabricTree : Timber.Tree() {
-
- override fun log(
- priority: Int,
- tag: String?,
- message: String,
- t: Throwable?
- ) {
- if (t != null) {
- Crashlytics.setString("crash_tag", tag)
- Crashlytics.logException(t)
- } else {
- Crashlytics.log(priority, tag, message)
- }
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/receivers/BootReceiver.java b/app/src/main/java/com/afollestad/nocknock/receivers/BootReceiver.java
new file mode 100644
index 0000000..4f5666f
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/receivers/BootReceiver.java
@@ -0,0 +1,28 @@
+package com.afollestad.nocknock.receivers;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.afollestad.inquiry.Inquiry;
+import com.afollestad.nocknock.api.ServerModel;
+import com.afollestad.nocknock.ui.MainActivity;
+import com.afollestad.nocknock.util.AlarmUtil;
+
+/**
+ * @author Aidan Follestad (afollestad)
+ */
+public class BootReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {
+ final Inquiry inq = Inquiry.newInstance(context, MainActivity.DB_NAME).build(false);
+ ServerModel[] models = inq
+ .selectFrom(MainActivity.SITES_TABLE_NAME, ServerModel.class)
+ .all();
+ AlarmUtil.setSiteChecks(context, models);
+ inq.destroyInstance();
+ }
+ }
+}
diff --git a/app/src/main/java/com/afollestad/nocknock/receivers/ConnectivityReceiver.java b/app/src/main/java/com/afollestad/nocknock/receivers/ConnectivityReceiver.java
new file mode 100644
index 0000000..7ceed62
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/receivers/ConnectivityReceiver.java
@@ -0,0 +1,25 @@
+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));
+ }
+ }
+}
diff --git a/app/src/main/java/com/afollestad/nocknock/services/CheckService.java b/app/src/main/java/com/afollestad/nocknock/services/CheckService.java
new file mode 100644
index 0000000..2b9ab94
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/services/CheckService.java
@@ -0,0 +1,241 @@
+package com.afollestad.nocknock.services;
+
+import android.app.IntentService;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.BitmapFactory;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.support.annotation.Nullable;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationManagerCompat;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.afollestad.bridge.Bridge;
+import com.afollestad.bridge.BridgeException;
+import com.afollestad.bridge.Response;
+import com.afollestad.inquiry.Inquiry;
+import com.afollestad.inquiry.Query;
+import com.afollestad.nocknock.BuildConfig;
+import com.afollestad.nocknock.R;
+import com.afollestad.nocknock.api.ServerModel;
+import com.afollestad.nocknock.api.ServerStatus;
+import com.afollestad.nocknock.api.ValidationMode;
+import com.afollestad.nocknock.ui.MainActivity;
+import com.afollestad.nocknock.ui.ViewSiteActivity;
+import com.afollestad.nocknock.util.JsUtil;
+import com.afollestad.nocknock.util.NetworkUtil;
+
+import java.util.Locale;
+
+/**
+ * @author Aidan Follestad (afollestad)
+ */
+@SuppressWarnings("CheckResult")
+public class CheckService extends IntentService {
+
+ public static String ACTION_CHECK_UPDATE = BuildConfig.APPLICATION_ID + ".CHECK_UPDATE";
+ public static String ACTION_RUNNING = BuildConfig.APPLICATION_ID + ".CHECK_RUNNING";
+ public static String MODEL_ID = "model_id";
+ public static String ONLY_WAITING = "only_waiting";
+ public static int NOTI_ID = 3456;
+
+ public CheckService() {
+ super("NockNockCheckService");
+ }
+
+ private static void LOG(String msg, Object... format) {
+ if (format != null)
+ msg = String.format(Locale.getDefault(), msg, format);
+ Log.v("NockNockService", msg);
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ Inquiry.newInstance(this, MainActivity.DB_NAME).build();
+ isRunning(true);
+ Bridge.config()
+ .defaultHeader("User-Agent", getString(R.string.app_name) + " (Android)");
+
+ final Query query = Inquiry.get(this)
+ .selectFrom(MainActivity.SITES_TABLE_NAME, ServerModel.class);
+ if (intent != null && intent.hasExtra(MODEL_ID)) {
+ query.where("_id = ?", intent.getLongExtra(MODEL_ID, -1));
+ } else if (intent != null && intent.getBooleanExtra(ONLY_WAITING, false)) {
+ query.where("status = ?", ServerStatus.WAITING);
+ }
+ final ServerModel[] sites = query.all();
+
+ if (sites == null || sites.length == 0) {
+ LOG("No sites added to check, service will terminate.");
+ isRunning(false);
+ stopSelf();
+ return;
+ }
+
+ LOG("Checking %d sites...", sites.length);
+ sendBroadcast(new Intent(ACTION_RUNNING));
+
+ for (ServerModel site : sites) {
+ LOG("Updating %s (%s) status to WAITING...", site.name, site.url);
+ site.status = ServerStatus.WAITING;
+ updateStatus(site);
+ }
+
+ if (NetworkUtil.hasInternet(this)) {
+ for (ServerModel site : sites) {
+ LOG("Checking %s (%s)...", site.name, site.url);
+ site.status = ServerStatus.CHECKING;
+ site.lastCheck = System.currentTimeMillis();
+ updateStatus(site);
+
+ try {
+ final Response response = Bridge.get(site.url)
+ .throwIfNotSuccess()
+ .cancellable(false)
+ .request()
+ .response();
+
+ site.reason = null;
+ site.status = ServerStatus.OK;
+
+ if (site.validationMode == ValidationMode.TERM_SEARCH) {
+ final String body = response.asString();
+ if (body == null || !body.contains(site.validationContent)) {
+ site.status = ServerStatus.ERROR;
+ site.reason = "Term \"" + site.validationContent + "\" not found in response body.";
+ }
+ } else if (site.validationMode == ValidationMode.JAVASCRIPT) {
+ final String body = response.asString();
+ site.reason = JsUtil.exec(site.validationContent, body);
+ if (site.reason != null && !site.toString().isEmpty())
+ site.status = ServerStatus.ERROR;
+ }
+
+ if (site.status == ServerStatus.ERROR)
+ showNotification(this, site);
+ } catch (BridgeException e) {
+ processError(e, site);
+ }
+ updateStatus(site);
+ }
+ } else {
+ LOG("No internet connection, waiting.");
+ }
+
+ isRunning(false);
+ LOG("Service is finished!");
+ }
+
+ private void processError(BridgeException e, ServerModel site) {
+ site.status = ServerStatus.OK;
+ site.reason = null;
+
+ switch (e.reason()) {
+ case BridgeException.REASON_REQUEST_CANCELLED:
+ // Shouldn't happen
+ break;
+ case BridgeException.REASON_REQUEST_FAILED:
+ case BridgeException.REASON_RESPONSE_UNPARSEABLE:
+ case BridgeException.REASON_RESPONSE_UNSUCCESSFUL:
+ case BridgeException.REASON_RESPONSE_IOERROR:
+ //noinspection ConstantConditions
+ if (e.response() != null && e.response().code() == 401) {
+ // Don't consider 401 unsuccessful here
+ site.reason = null;
+ } else {
+ site.status = ServerStatus.ERROR;
+ site.reason = e.getMessage();
+ }
+ break;
+ case BridgeException.REASON_REQUEST_TIMEOUT:
+ site.status = ServerStatus.ERROR;
+ site.reason = getString(R.string.timeout);
+ break;
+ case BridgeException.REASON_RESPONSE_VALIDATOR_ERROR:
+ case BridgeException.REASON_RESPONSE_VALIDATOR_FALSE:
+ // Not used
+ break;
+ }
+
+ if (site.status != ServerStatus.OK) {
+ LOG("%s error: %s", site.name, site.reason);
+ showNotification(this, site);
+ }
+ }
+
+ private void updateStatus(ServerModel site) {
+ Inquiry.get(this)
+ .update(MainActivity.SITES_TABLE_NAME, ServerModel.class)
+ .where("_id = ?", site.id)
+ .values(site)
+ .run();
+ sendBroadcast(new Intent(ACTION_CHECK_UPDATE)
+ .putExtra("model", site));
+ }
+
+ private void isRunning(boolean running) {
+ PreferenceManager.getDefaultSharedPreferences(this)
+ .edit().putBoolean("check_service_running", running).commit();
+ }
+
+ public static boolean isRunning(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean("check_service_running", false);
+ }
+
+ public static void isAppOpen(Context context, boolean open) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit().putBoolean("is_app_open", open).commit();
+ }
+
+ public static boolean isAppOpen(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean("is_app_open", false);
+ }
+
+ private static void showNotification(Context context, ServerModel site) {
+ if (isAppOpen(context)) {
+ // Don't show notifications while the app is open
+ return;
+ }
+
+ final NotificationManagerCompat nm = NotificationManagerCompat.from(context);
+ final PendingIntent openIntent = PendingIntent.getActivity(context, 9669,
+ new Intent(context, ViewSiteActivity.class)
+ .putExtra("model", site)
+ .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
+ PendingIntent.FLAG_CANCEL_CURRENT);
+ final Notification noti = new NotificationCompat.Builder(context)
+ .setContentTitle(site.name)
+ .setContentText(context.getString(R.string.something_wrong))
+ .setContentIntent(openIntent)
+ .setSmallIcon(R.drawable.ic_notification)
+ .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher))
+ .setPriority(Notification.PRIORITY_HIGH)
+ .setAutoCancel(true)
+ .setDefaults(Notification.DEFAULT_VIBRATE)
+ .build();
+ nm.notify(site.url, NOTI_ID, noti);
+ }
+
+ @Override
+ public void onDestroy() {
+ try {
+ Inquiry.destroy(this);
+ } catch (Throwable t2) {
+ t2.printStackTrace();
+ }
+ super.onDestroy();
+ }
+}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/AddSiteActivity.java b/app/src/main/java/com/afollestad/nocknock/ui/AddSiteActivity.java
new file mode 100644
index 0000000..cb54ece
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/ui/AddSiteActivity.java
@@ -0,0 +1,255 @@
+package com.afollestad.nocknock.ui;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.design.widget.TextInputLayout;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.util.Patterns;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.ViewTreeObserver;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import com.afollestad.nocknock.R;
+import com.afollestad.nocknock.api.ServerModel;
+import com.afollestad.nocknock.api.ServerStatus;
+import com.afollestad.nocknock.api.ValidationMode;
+
+/**
+ * @author Aidan Follestad (afollestad)
+ */
+public class AddSiteActivity extends AppCompatActivity implements View.OnClickListener {
+
+ private View rootLayout;
+ private Toolbar toolbar;
+
+ private TextInputLayout nameTiLayout;
+ private EditText inputName;
+ private TextInputLayout urlTiLayout;
+ private EditText inputUrl;
+ private EditText inputInterval;
+ private Spinner spinnerInterval;
+ private TextView textUrlWarning;
+ private Spinner responseValidationSpinner;
+
+ private boolean isClosing;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_addsite);
+
+ rootLayout = findViewById(R.id.rootView);
+ nameTiLayout = (TextInputLayout) findViewById(R.id.nameTiLayout);
+ inputName = (EditText) findViewById(R.id.inputName);
+ urlTiLayout = (TextInputLayout) findViewById(R.id.urlTiLayout);
+ inputUrl = (EditText) findViewById(R.id.inputUrl);
+ textUrlWarning = (TextView) findViewById(R.id.textUrlWarning);
+ inputInterval = (EditText) findViewById(R.id.checkIntervalInput);
+ spinnerInterval = (Spinner) findViewById(R.id.checkIntervalSpinner);
+ responseValidationSpinner = (Spinner) findViewById(R.id.responseValidationMode);
+
+ toolbar = (Toolbar) findViewById(R.id.toolbar);
+ toolbar.setNavigationOnClickListener(view -> closeActivityWithReveal());
+
+ if (savedInstanceState == null) {
+ rootLayout.setVisibility(View.INVISIBLE);
+ ViewTreeObserver viewTreeObserver = rootLayout.getViewTreeObserver();
+ if (viewTreeObserver.isAlive()) {
+ viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ circularRevealActivity();
+ rootLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ }
+ });
+ }
+ }
+
+ ArrayAdapter 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 validationOptionsAdapter = new ArrayAdapter<>(this, R.layout.list_item_spinner,
+ getResources().getStringArray(R.array.response_validation_options));
+ validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown);
+ responseValidationSpinner.setAdapter(validationOptionsAdapter);
+ responseValidationSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> adapterView, View view, int i, long l) {
+ final View searchTerm = findViewById(R.id.responseValidationSearchTerm);
+ final View javascript = findViewById(R.id.responseValidationScript);
+ final TextView modeDesc = (TextView) findViewById(R.id.validationModeDescription);
+
+ searchTerm.setVisibility(i == 1 ? View.VISIBLE : View.GONE);
+ javascript.setVisibility(i == 2 ? View.VISIBLE : View.GONE);
+
+ switch (i) {
+ case 0:
+ modeDesc.setText(R.string.validation_mode_status_desc);
+ break;
+ case 1:
+ modeDesc.setText(R.string.validation_mode_term_desc);
+ break;
+ case 2:
+ modeDesc.setText(R.string.validation_mode_javascript_desc);
+ break;
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> adapterView) {
+ }
+ });
+
+ findViewById(R.id.doneBtn).setOnClickListener(this);
+ }
+
+ @Override
+ public void onBackPressed() {
+ closeActivityWithReveal();
+ }
+
+ private void closeActivityWithReveal() {
+ if (isClosing) return;
+ isClosing = true;
+ final int fabSize = getIntent().getIntExtra("fab_size", toolbar.getMeasuredHeight());
+ final int cx = (int) getIntent().getFloatExtra("fab_x", rootLayout.getMeasuredWidth() / 2) + (fabSize / 2);
+ final int cy = (int) getIntent().getFloatExtra("fab_y", rootLayout.getMeasuredHeight() / 2) + toolbar.getMeasuredHeight() + (fabSize / 2);
+ float initialRadius = Math.max(cx, cy);
+
+ final Animator circularReveal = ViewAnimationUtils.createCircularReveal(rootLayout, cx, cy, initialRadius, 0);
+ circularReveal.setDuration(300);
+ circularReveal.setInterpolator(new AccelerateInterpolator());
+ circularReveal.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ rootLayout.setVisibility(View.INVISIBLE);
+ finish();
+ overridePendingTransition(0, 0);
+ }
+ });
+
+ circularReveal.start();
+ }
+
+ private void circularRevealActivity() {
+ final int cx = rootLayout.getMeasuredWidth() / 2;
+ final int cy = rootLayout.getMeasuredHeight() / 2;
+ final float finalRadius = Math.max(cx, cy);
+ final Animator circularReveal = ViewAnimationUtils.createCircularReveal(rootLayout, cx, cy, 0, finalRadius);
+
+ circularReveal.setDuration(300);
+ circularReveal.setInterpolator(new DecelerateInterpolator());
+
+ rootLayout.setVisibility(View.VISIBLE);
+ circularReveal.start();
+ }
+
+ // Done button
+ @Override
+ public void onClick(View view) {
+ isClosing = true;
+
+ ServerModel model = new ServerModel();
+ model.name = inputName.getText().toString().trim();
+ model.url = inputUrl.getText().toString().trim();
+ model.status = ServerStatus.WAITING;
+
+ if (model.name.isEmpty()) {
+ nameTiLayout.setError(getString(R.string.please_enter_name));
+ isClosing = false;
+ return;
+ } else {
+ nameTiLayout.setError(null);
+ }
+
+ if (model.url.isEmpty()) {
+ urlTiLayout.setError(getString(R.string.please_enter_url));
+ isClosing = false;
+ return;
+ } else {
+ urlTiLayout.setError(null);
+ if (!Patterns.WEB_URL.matcher(model.url).find()) {
+ urlTiLayout.setError(getString(R.string.please_enter_valid_url));
+ isClosing = false;
+ return;
+ } else {
+ final Uri uri = Uri.parse(model.url);
+ if (uri.getScheme() == null)
+ model.url = "http://" + model.url;
+ }
+ }
+
+ String intervalStr = inputInterval.getText().toString().trim();
+ if (intervalStr.isEmpty()) intervalStr = "0";
+ model.checkInterval = Integer.parseInt(intervalStr);
+
+ switch (spinnerInterval.getSelectedItemPosition()) {
+ case 0: // minutes
+ model.checkInterval *= (60 * 1000);
+ break;
+ case 1: // hours
+ model.checkInterval *= (60 * 60 * 1000);
+ break;
+ case 2: // days
+ model.checkInterval *= (60 * 60 * 24 * 1000);
+ break;
+ default: // weeks
+ model.checkInterval *= (60 * 60 * 24 * 7 * 1000);
+ break;
+ }
+
+ model.lastCheck = System.currentTimeMillis() - model.checkInterval;
+
+ switch (responseValidationSpinner.getSelectedItemPosition()) {
+ case 0:
+ model.validationMode = ValidationMode.STATUS_CODE;
+ model.validationContent = null;
+ break;
+ case 1:
+ model.validationMode = ValidationMode.TERM_SEARCH;
+ model.validationContent = ((EditText) findViewById(R.id.responseValidationSearchTerm)).getText().toString().trim();
+ break;
+ case 2:
+ model.validationMode = ValidationMode.JAVASCRIPT;
+ model.validationContent = ((EditText) findViewById(R.id.responseValidationScriptInput)).getText().toString().trim();
+ break;
+ }
+
+ setResult(RESULT_OK, new Intent()
+ .putExtra("model", model));
+ finish();
+ overridePendingTransition(R.anim.fade_out, R.anim.fade_out);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/DarkModeSwitchActivity.kt b/app/src/main/java/com/afollestad/nocknock/ui/DarkModeSwitchActivity.kt
deleted file mode 100644
index 3220567..0000000
--- a/app/src/main/java/com/afollestad/nocknock/ui/DarkModeSwitchActivity.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.ui
-
-import android.content.res.Configuration
-import android.os.Build
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import com.afollestad.nocknock.R
-import com.afollestad.nocknock.koin.PREF_DARK_MODE
-import com.afollestad.nocknock.ui.NightMode.DISABLED
-import com.afollestad.nocknock.ui.NightMode.ENABLED
-import com.afollestad.nocknock.ui.NightMode.UNKNOWN
-import com.afollestad.nocknock.utilities.rx.attachLifecycle
-import com.afollestad.rxkprefs.Pref
-import org.koin.android.ext.android.inject
-import timber.log.Timber.d as log
-
-/** @author Aidan Follestad (afollestad) */
-abstract class DarkModeSwitchActivity : AppCompatActivity() {
-
- private var isDarkModeEnabled: Boolean = false
- private val darkModePref by inject>(name = PREF_DARK_MODE)
-
- override fun onCreate(savedInstanceState: Bundle?) {
- isDarkModeEnabled = isDarkMode()
- setTheme(themeRes())
- super.onCreate(savedInstanceState)
-
- if (getCurrentNightMode() == UNKNOWN) {
- darkModePref.observe()
- .filter { it != isDarkModeEnabled }
- .subscribe {
- log("Theme changed, recreating Activity.")
- recreate()
- }
- .attachLifecycle(this)
- }
- }
-
- protected fun getCurrentNightMode(): NightMode {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
- return UNKNOWN
- }
- return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
- Configuration.UI_MODE_NIGHT_YES -> return ENABLED
- Configuration.UI_MODE_NIGHT_NO -> return DISABLED
- else -> UNKNOWN
- }
- }
-
- protected fun isDarkMode(): Boolean {
- return when (getCurrentNightMode()) {
- ENABLED -> true
- DISABLED -> false
- else -> darkModePref.get()
- }
- }
-
- protected fun toggleDarkMode() = setDarkMode(!isDarkMode())
-
- private fun setDarkMode(darkMode: Boolean) = darkModePref.set(darkMode)
-
- private fun themeRes() = if (isDarkMode()) {
- R.style.AppTheme_Dark
- } else {
- R.style.AppTheme
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/MainActivity.java b/app/src/main/java/com/afollestad/nocknock/ui/MainActivity.java
new file mode 100644
index 0000000..88f8ba8
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/ui/MainActivity.java
@@ -0,0 +1,326 @@
+package com.afollestad.nocknock.ui;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.annotation.SuppressLint;
+import android.app.ActivityOptions;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.graphics.Path;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.app.NotificationManagerCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.Html;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.animation.PathInterpolator;
+import android.widget.TextView;
+
+import com.afollestad.bridge.Bridge;
+import com.afollestad.inquiry.Inquiry;
+import com.afollestad.materialdialogs.MaterialDialog;
+import com.afollestad.nocknock.R;
+import com.afollestad.nocknock.adapter.ServerAdapter;
+import com.afollestad.nocknock.api.ServerModel;
+import com.afollestad.nocknock.api.ValidationMode;
+import com.afollestad.nocknock.dialogs.AboutDialog;
+import com.afollestad.nocknock.services.CheckService;
+import com.afollestad.nocknock.util.AlarmUtil;
+import com.afollestad.nocknock.util.MathUtil;
+import com.afollestad.nocknock.views.DividerItemDecoration;
+
+public class MainActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener, View.OnClickListener, ServerAdapter.ClickListener {
+
+ private final static int ADD_SITE_RQ = 6969;
+ private final static int VIEW_SITE_RQ = 6923;
+ public final static String DB_NAME = "nock_nock";
+ public final static String SITES_TABLE_NAME_OLD = "sites";
+ public final static String SITES_TABLE_NAME = "site_models";
+
+ private FloatingActionButton mFab;
+ private RecyclerView mList;
+ private ServerAdapter mAdapter;
+ private TextView mEmptyText;
+ private SwipeRefreshLayout mRefreshLayout;
+
+ private ObjectAnimator mFabAnimator;
+ private float mOrigFabX;
+ private float mOrigFabY;
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.v("MainActivity", "Received " + intent.getAction());
+ if (CheckService.ACTION_RUNNING.equals(intent.getAction())) {
+ if (mRefreshLayout != null)
+ mRefreshLayout.setRefreshing(false);
+ } else {
+ final ServerModel model = (ServerModel) intent.getSerializableExtra("model");
+ if (mAdapter != null && mList != null && model != null) {
+ mList.post(() -> mAdapter.update(model));
+ }
+ }
+ }
+ };
+
+ @SuppressLint("CommitPrefEdits")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ mAdapter = new ServerAdapter(this);
+ mEmptyText = (TextView) findViewById(R.id.emptyText);
+
+ mList = (RecyclerView) findViewById(R.id.list);
+ mList.setLayoutManager(new LinearLayoutManager(this));
+ mList.setAdapter(mAdapter);
+ mList.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
+
+ mRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefreshLayout);
+ mRefreshLayout.setOnRefreshListener(this);
+ mRefreshLayout.setColorSchemeColors(ContextCompat.getColor(this, R.color.md_green),
+ ContextCompat.getColor(this, R.color.md_yellow),
+ ContextCompat.getColor(this, R.color.md_red));
+
+ mFab = (FloatingActionButton) findViewById(R.id.fab);
+ mFab.setOnClickListener(this);
+
+ Inquiry.newInstance(this, DB_NAME).build();
+ Bridge.config()
+ .defaultHeader("User-Agent", getString(R.string.app_name) + " (Android)");
+
+ final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ if (!sp.getBoolean("migrated_db", false)) {
+ final Inquiry mdb = Inquiry.newInstance(this, DB_NAME)
+ .instanceName("migrate_db")
+ .build(false);
+ final ServerModel[] models = Inquiry.get(this)
+ .selectFrom(SITES_TABLE_NAME_OLD, ServerModel.class)
+ .projection("_id", "name", "url", "status", "checkInterval", "lastCheck", "reason")
+ .all();
+ if (models != null) {
+ Log.d("SiteMigration", "Migrating " + models.length + " sites to the new table.");
+ for (ServerModel model : models) {
+ model.validationMode = ValidationMode.STATUS_CODE;
+ model.validationContent = null;
+ }
+ //noinspection CheckResult
+ mdb.insertInto(SITES_TABLE_NAME, ServerModel.class)
+ .values(models)
+ .run();
+ mdb.dropTable(SITES_TABLE_NAME_OLD);
+ }
+ sp.edit().putBoolean("migrated_db", true).commit();
+ }
+ }
+
+ private void showRefreshTutorial() {
+ if (mAdapter.getItemCount() == 0) return;
+ final SharedPreferences pr = PreferenceManager.getDefaultSharedPreferences(this);
+ if (pr.getBoolean("shown_swipe_refresh_tutorial", false)) return;
+
+ mFab.hide();
+ final View tutorialView = findViewById(R.id.swipeRefreshTutorial);
+ tutorialView.setVisibility(View.VISIBLE);
+ tutorialView.setAlpha(0f);
+ tutorialView.animate().cancel();
+ tutorialView.animate().setDuration(300).alpha(1f).start();
+
+ findViewById(R.id.understoodBtn).setOnClickListener(view -> {
+ view.setOnClickListener(null);
+ findViewById(R.id.swipeRefreshTutorial).setVisibility(View.GONE);
+ pr.edit().putBoolean("shown_swipe_refresh_tutorial", true).commit();
+ mFab.show();
+ });
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ CheckService.isAppOpen(this, true);
+
+ try {
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(CheckService.ACTION_CHECK_UPDATE);
+ filter.addAction(CheckService.ACTION_RUNNING);
+ registerReceiver(mReceiver, filter);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+
+ refreshModels();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ CheckService.isAppOpen(this, false);
+
+ if (isFinishing()) {
+ Inquiry.destroy(this);
+ }
+
+ NotificationManagerCompat.from(this).cancel(CheckService.NOTI_ID);
+ try {
+ unregisterReceiver(mReceiver);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ private void refreshModels() {
+ mAdapter.clear();
+ mEmptyText.setVisibility(View.VISIBLE);
+ Inquiry.get(this)
+ .selectFrom(SITES_TABLE_NAME, ServerModel.class)
+ .all(this::setModels);
+ }
+
+ private void setModels(ServerModel[] models) {
+ mAdapter.set(models);
+ mEmptyText.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
+ AlarmUtil.setSiteChecks(this, models);
+ showRefreshTutorial();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_main, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.about) {
+ AboutDialog.show(this);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onRefresh() {
+ if (CheckService.isRunning(this)) {
+ mRefreshLayout.setRefreshing(false);
+ return;
+ }
+ startService(new Intent(this, CheckService.class));
+ }
+
+ // FAB clicked
+ @Override
+ public void onClick(View view) {
+ mOrigFabX = mFab.getX();
+ mOrigFabY = mFab.getY();
+ final Path curve = MathUtil.bezierCurve(mFab, mList);
+ if (mFabAnimator != null)
+ mFabAnimator.cancel();
+ mFabAnimator = ObjectAnimator.ofFloat(view, View.X, View.Y, curve);
+ mFabAnimator.setInterpolator(new PathInterpolator(.5f, .5f));
+ mFabAnimator.setDuration(300);
+ mFabAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ startActivityForResult(new Intent(MainActivity.this, AddSiteActivity.class)
+ .putExtra("fab_x", mOrigFabX)
+ .putExtra("fab_y", mOrigFabY)
+ .putExtra("fab_size", mFab.getMeasuredWidth())
+ .addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION), ADD_SITE_RQ);
+ mFab.postDelayed(() -> {
+ mFab.setX(mOrigFabX);
+ mFab.setY(mOrigFabY);
+ }, 600);
+ }
+ });
+ mFabAnimator.start();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_OK) {
+ final ServerModel model = (ServerModel) data.getSerializableExtra("model");
+ if (requestCode == ADD_SITE_RQ) {
+ mAdapter.add(model);
+ mEmptyText.setVisibility(View.GONE);
+ Inquiry.get(this).insertInto(SITES_TABLE_NAME, ServerModel.class)
+ .values(model)
+ .run(inserted -> {
+ AlarmUtil.setSiteChecks(MainActivity.this, model);
+ checkSite(MainActivity.this, model);
+ });
+ } else if (requestCode == VIEW_SITE_RQ) {
+ mAdapter.update(model);
+ AlarmUtil.setSiteChecks(MainActivity.this, model);
+ checkSite(MainActivity.this, model);
+ }
+ }
+ }
+
+ public static void removeSite(final Context context, final ServerModel model, final Runnable onRemoved) {
+ new MaterialDialog.Builder(context)
+ .title(R.string.remove_site)
+ .content(Html.fromHtml(context.getString(R.string.remove_site_prompt, model.name)))
+ .positiveText(R.string.remove)
+ .negativeText(android.R.string.cancel)
+ .onPositive((dialog, which) -> {
+ AlarmUtil.cancelSiteChecks(context, model);
+ final NotificationManagerCompat nm = NotificationManagerCompat.from(context);
+ nm.cancel(model.url, CheckService.NOTI_ID);
+ //noinspection CheckResult
+ final Inquiry rinq = Inquiry.newInstance(context, DB_NAME)
+ .instanceName("remove_site")
+ .build(false);
+ //noinspection CheckResult
+ rinq.deleteFrom(SITES_TABLE_NAME, ServerModel.class)
+ .where("_id = ?", model.id)
+ .run();
+ rinq.destroyInstance();
+ if (onRemoved != null)
+ onRemoved.run();
+ }).show();
+ }
+
+ public static void checkSite(Context context, ServerModel model) {
+ context.startService(new Intent(context, CheckService.class)
+ .putExtra(CheckService.MODEL_ID, model.id));
+ }
+
+ @Override
+ public void onSiteSelected(final int index, final ServerModel model, boolean longClick) {
+ if (longClick) {
+ new MaterialDialog.Builder(this)
+ .title(R.string.options)
+ .items(R.array.site_long_options)
+ .negativeText(android.R.string.cancel)
+ .itemsCallback((dialog, itemView, which, text) -> {
+ if (which == 0) {
+ checkSite(MainActivity.this, model);
+ } else {
+ removeSite(MainActivity.this, model, () -> {
+ mAdapter.remove(index);
+ mEmptyText.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
+ });
+ }
+ }).show();
+ } else {
+ startActivityForResult(new Intent(this, ViewSiteActivity.class)
+ .putExtra("model", model), VIEW_SITE_RQ,
+ ActivityOptions.makeSceneTransitionAnimation(this).toBundle());
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/NightMode.kt b/app/src/main/java/com/afollestad/nocknock/ui/NightMode.kt
deleted file mode 100644
index 2930fea..0000000
--- a/app/src/main/java/com/afollestad/nocknock/ui/NightMode.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.ui
-
-/** @author Aidan Follestad (@afollestad) */
-enum class NightMode {
- /** Night mode is on at the system level. */
- ENABLED,
- /** Night mode is off at the system level. */
- DISABLED,
- /** We don't know about night mode, fallback to custom impl. */
- UNKNOWN
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/ScopedViewModel.kt b/app/src/main/java/com/afollestad/nocknock/ui/ScopedViewModel.kt
deleted file mode 100644
index 750173a..0000000
--- a/app/src/main/java/com/afollestad/nocknock/ui/ScopedViewModel.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.ui
-
-import androidx.lifecycle.ViewModel
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import org.jetbrains.annotations.TestOnly
-
-/** @author Aidan Follestad (@afollestad) */
-abstract class ScopedViewModel(mainDispatcher: CoroutineDispatcher) : ViewModel() {
-
- private val job = Job()
- protected val scope = CoroutineScope(job + mainDispatcher)
-
- override fun onCleared() {
- super.onCleared()
- job.cancel()
- }
-
- @TestOnly open fun destroy() = job.cancel()
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/ViewSiteActivity.java b/app/src/main/java/com/afollestad/nocknock/ui/ViewSiteActivity.java
new file mode 100644
index 0000000..5edbef3
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/ui/ViewSiteActivity.java
@@ -0,0 +1,340 @@
+package com.afollestad.nocknock.ui;
+
+import android.annotation.SuppressLint;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.util.Log;
+import android.util.Patterns;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import com.afollestad.bridge.Bridge;
+import com.afollestad.inquiry.Inquiry;
+import com.afollestad.nocknock.R;
+import com.afollestad.nocknock.api.ServerModel;
+import com.afollestad.nocknock.api.ServerStatus;
+import com.afollestad.nocknock.api.ValidationMode;
+import com.afollestad.nocknock.services.CheckService;
+import com.afollestad.nocknock.util.TimeUtil;
+import com.afollestad.nocknock.views.StatusImageView;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * @author Aidan Follestad (afollestad)
+ */
+public class ViewSiteActivity extends AppCompatActivity implements View.OnClickListener, Toolbar.OnMenuItemClickListener {
+
+ private StatusImageView iconStatus;
+ private EditText inputName;
+ private EditText inputUrl;
+ private EditText inputCheckInterval;
+ private Spinner checkIntervalSpinner;
+ private TextView textLastCheckResult;
+ private TextView textNextCheck;
+ private TextView textUrlWarning;
+ private Spinner responseValidationSpinner;
+
+ private ServerModel mModel;
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.v("ViewSiteActivity", "Received " + intent.getAction());
+ final ServerModel model = (ServerModel) intent.getSerializableExtra("model");
+ if (model != null) {
+ mModel = model;
+ update();
+ }
+ }
+ };
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_viewsite);
+
+ final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ toolbar.setNavigationOnClickListener(view -> finish());
+ toolbar.inflateMenu(R.menu.menu_viewsite);
+ toolbar.setOnMenuItemClickListener(this);
+
+ iconStatus = (StatusImageView) findViewById(R.id.iconStatus);
+ inputName = (EditText) findViewById(R.id.inputName);
+ inputUrl = (EditText) findViewById(R.id.inputUrl);
+ textUrlWarning = (TextView) findViewById(R.id.textUrlWarning);
+ inputCheckInterval = (EditText) findViewById(R.id.checkIntervalInput);
+ checkIntervalSpinner = (Spinner) findViewById(R.id.checkIntervalSpinner);
+ textLastCheckResult = (TextView) findViewById(R.id.textLastCheckResult);
+ textNextCheck = (TextView) findViewById(R.id.textNextCheck);
+ responseValidationSpinner = (Spinner) findViewById(R.id.responseValidationMode);
+
+ ArrayAdapter 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 validationOptionsAdapter = new ArrayAdapter<>(this, R.layout.list_item_spinner,
+ getResources().getStringArray(R.array.response_validation_options));
+ validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown);
+ responseValidationSpinner.setAdapter(validationOptionsAdapter);
+ responseValidationSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> adapterView, View view, int i, long l) {
+ final View searchTerm = findViewById(R.id.responseValidationSearchTerm);
+ final View javascript = findViewById(R.id.responseValidationScript);
+ final TextView modeDesc = (TextView) findViewById(R.id.validationModeDescription);
+
+ searchTerm.setVisibility(i == 1 ? View.VISIBLE : View.GONE);
+ javascript.setVisibility(i == 2 ? View.VISIBLE : View.GONE);
+
+ switch (i) {
+ case 0:
+ modeDesc.setText(R.string.validation_mode_status_desc);
+ break;
+ case 1:
+ modeDesc.setText(R.string.validation_mode_term_desc);
+ break;
+ case 2:
+ modeDesc.setText(R.string.validation_mode_javascript_desc);
+ break;
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> adapterView) {
+ }
+ });
+
+ mModel = (ServerModel) getIntent().getSerializableExtra("model");
+ update();
+
+ Bridge.config()
+ .defaultHeader("User-Agent", getString(R.string.app_name) + " (Android)");
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ if (intent != null && intent.hasExtra("model")) {
+ mModel = (ServerModel) intent.getSerializableExtra("model");
+ update();
+ }
+ }
+
+ @SuppressLint({"SetTextI18n", "SwitchIntDef"})
+ private void update() {
+ final SimpleDateFormat df = new SimpleDateFormat("MMMM dd, hh:mm:ss a", Locale.getDefault());
+
+ iconStatus.setStatus(mModel.status);
+ inputName.setText(mModel.name);
+ inputUrl.setText(mModel.url);
+
+ if (mModel.lastCheck == 0) {
+ textLastCheckResult.setText(R.string.none);
+ } else {
+ switch (mModel.status) {
+ case ServerStatus.CHECKING:
+ textLastCheckResult.setText(R.string.checking_status);
+ break;
+ case ServerStatus.ERROR:
+ textLastCheckResult.setText(mModel.reason);
+ break;
+ case ServerStatus.OK:
+ textLastCheckResult.setText(R.string.everything_checks_out);
+ break;
+ case ServerStatus.WAITING:
+ textLastCheckResult.setText(R.string.waiting);
+ break;
+ }
+ }
+
+ if (mModel.checkInterval == 0) {
+ textNextCheck.setText(R.string.none_turned_off);
+ inputCheckInterval.setText("");
+ checkIntervalSpinner.setSelection(0);
+ } else {
+ long lastCheck = mModel.lastCheck;
+ if (lastCheck == 0) lastCheck = System.currentTimeMillis();
+ textNextCheck.setText(df.format(new Date(lastCheck + mModel.checkInterval)));
+
+ if (mModel.checkInterval >= TimeUtil.WEEK) {
+ inputCheckInterval.setText(Integer.toString((int) Math.ceil(((float) mModel.checkInterval / (float) TimeUtil.WEEK))));
+ checkIntervalSpinner.setSelection(3);
+ } else if (mModel.checkInterval >= TimeUtil.DAY) {
+ inputCheckInterval.setText(Integer.toString((int) Math.ceil(((float) mModel.checkInterval / (float) TimeUtil.DAY))));
+ checkIntervalSpinner.setSelection(2);
+ } else if (mModel.checkInterval >= TimeUtil.HOUR) {
+ inputCheckInterval.setText(Integer.toString((int) Math.ceil(((float) mModel.checkInterval / (float) TimeUtil.HOUR))));
+ checkIntervalSpinner.setSelection(1);
+ } else if (mModel.checkInterval >= TimeUtil.MINUTE) {
+ inputCheckInterval.setText(Integer.toString((int) Math.ceil(((float) mModel.checkInterval / (float) TimeUtil.MINUTE))));
+ checkIntervalSpinner.setSelection(0);
+ } else {
+ inputCheckInterval.setText("0");
+ checkIntervalSpinner.setSelection(0);
+ }
+ }
+
+ responseValidationSpinner.setSelection(mModel.validationMode - 1);
+ switch (mModel.validationMode) {
+ case ValidationMode.TERM_SEARCH:
+ ((TextView) findViewById(R.id.responseValidationSearchTerm)).setText(mModel.validationContent);
+ break;
+ case ValidationMode.JAVASCRIPT:
+ ((TextView) findViewById(R.id.responseValidationScriptInput)).setText(mModel.validationContent);
+ break;
+ }
+
+ findViewById(R.id.doneBtn).setOnClickListener(this);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ try {
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(CheckService.ACTION_CHECK_UPDATE);
+ // filter.addAction(CheckService.ACTION_RUNNING);
+ registerReceiver(mReceiver, filter);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ try {
+ unregisterReceiver(mReceiver);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ private void performSave(boolean withValidation) {
+ mModel.name = inputName.getText().toString().trim();
+ mModel.url = inputUrl.getText().toString().trim();
+ mModel.status = ServerStatus.WAITING;
+
+ if (withValidation && mModel.name.isEmpty()) {
+ inputName.setError(getString(R.string.please_enter_name));
+ return;
+ } else {
+ inputName.setError(null);
+ }
+
+ if (withValidation && mModel.url.isEmpty()) {
+ inputUrl.setError(getString(R.string.please_enter_url));
+ return;
+ } else {
+ inputUrl.setError(null);
+ if (withValidation && !Patterns.WEB_URL.matcher(mModel.url).find()) {
+ inputUrl.setError(getString(R.string.please_enter_valid_url));
+ return;
+ } else {
+ final Uri uri = Uri.parse(mModel.url);
+ if (uri.getScheme() == null)
+ mModel.url = "http://" + mModel.url;
+ }
+ }
+
+ String intervalStr = inputCheckInterval.getText().toString().trim();
+ if (intervalStr.isEmpty()) intervalStr = "0";
+ mModel.checkInterval = Integer.parseInt(intervalStr);
+
+ switch (checkIntervalSpinner.getSelectedItemPosition()) {
+ case 0: // minutes
+ mModel.checkInterval *= (60 * 1000);
+ break;
+ case 1: // hours
+ mModel.checkInterval *= (60 * 60 * 1000);
+ break;
+ case 2: // days
+ mModel.checkInterval *= (60 * 60 * 24 * 1000);
+ break;
+ default: // weeks
+ mModel.checkInterval *= (60 * 60 * 24 * 7 * 1000);
+ break;
+ }
+
+ mModel.lastCheck = System.currentTimeMillis() - mModel.checkInterval;
+
+ switch (responseValidationSpinner.getSelectedItemPosition()) {
+ case 0:
+ mModel.validationMode = ValidationMode.STATUS_CODE;
+ mModel.validationContent = null;
+ break;
+ case 1:
+ mModel.validationMode = ValidationMode.TERM_SEARCH;
+ mModel.validationContent = ((EditText) findViewById(R.id.responseValidationSearchTerm)).getText().toString().trim();
+ break;
+ case 2:
+ mModel.validationMode = ValidationMode.JAVASCRIPT;
+ mModel.validationContent = ((EditText) findViewById(R.id.responseValidationScriptInput)).getText().toString().trim();
+ break;
+ }
+
+ final Inquiry inq = Inquiry.newInstance(this, MainActivity.DB_NAME)
+ .build(false);
+ //noinspection CheckResult
+ inq.update(MainActivity.SITES_TABLE_NAME, ServerModel.class)
+ .where("_id = ?", mModel.id)
+ .values(mModel)
+ .run();
+ inq.destroyInstance();
+ }
+
+ // Save button
+ @Override
+ public void onClick(View view) {
+ performSave(true);
+ setResult(RESULT_OK, new Intent().putExtra("model", mModel));
+ finish();
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.refresh:
+ performSave(false);
+ MainActivity.checkSite(this, mModel);
+ return true;
+ case R.id.remove:
+ MainActivity.removeSite(this, mModel, this::finish);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt b/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt
deleted file mode 100644
index e15a29f..0000000
--- a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt
+++ /dev/null
@@ -1,235 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.ui.addsite
-
-import android.annotation.SuppressLint
-import android.content.Intent
-import android.content.Intent.ACTION_OPEN_DOCUMENT
-import android.content.Intent.CATEGORY_OPENABLE
-import android.os.Bundle
-import android.widget.ArrayAdapter
-import androidx.lifecycle.Observer
-import com.afollestad.nocknock.R
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.data.model.ValidationMode
-import com.afollestad.nocknock.ui.DarkModeSwitchActivity
-import com.afollestad.nocknock.ui.viewsite.KEY_SITE
-import com.afollestad.nocknock.utilities.ext.onTextChanged
-import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
-import com.afollestad.nocknock.utilities.livedata.distinct
-import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
-import com.afollestad.nocknock.viewcomponents.ext.isVisibleCondition
-import com.afollestad.nocknock.viewcomponents.ext.onScroll
-import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
-import com.afollestad.nocknock.viewcomponents.livedata.toViewText
-import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
-import com.afollestad.vvalidator.form
-import com.afollestad.vvalidator.form.Form
-import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout
-import kotlinx.android.synthetic.main.activity_addsite.headersLayout
-import kotlinx.android.synthetic.main.activity_addsite.inputName
-import kotlinx.android.synthetic.main.activity_addsite.inputTags
-import kotlinx.android.synthetic.main.activity_addsite.inputUrl
-import kotlinx.android.synthetic.main.activity_addsite.loadingProgress
-import kotlinx.android.synthetic.main.activity_addsite.responseTimeoutInput
-import kotlinx.android.synthetic.main.activity_addsite.responseValidationMode
-import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchTerm
-import kotlinx.android.synthetic.main.activity_addsite.retryPolicyLayout
-import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout
-import kotlinx.android.synthetic.main.activity_addsite.scrollView
-import kotlinx.android.synthetic.main.activity_addsite.sslCertificateBrowse
-import kotlinx.android.synthetic.main.activity_addsite.sslCertificateInput
-import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning
-import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
-import kotlinx.android.synthetic.main.include_app_bar.toolbar
-import org.koin.androidx.viewmodel.ext.android.viewModel
-import kotlinx.android.synthetic.main.include_app_bar.app_toolbar as appToolbar
-import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
-
-/** @author Aidan Follestad (@afollestad) */
-class AddSiteActivity : DarkModeSwitchActivity() {
- companion object {
- private const val SELECT_CERT_FILE_RQ = 23
- }
-
- private val viewModel by viewModel()
- private lateinit var validationForm: Form
-
- @SuppressLint("SetTextI18n")
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_addsite)
- setupUi()
- setupValidation()
-
- lifecycle.addObserver(viewModel)
-
- // Populate view model with initial data
- val model = intent.getSerializableExtra(KEY_SITE) as? Site
- model?.let { viewModel.prePopulateFromModel(model) }
-
- // Loading
- loadingProgress.observe(this, viewModel.onIsLoading())
-
- // Name
- inputName.attachLiveData(this, viewModel.name)
-
- // Tags
- inputTags.attachLiveData(this, viewModel.tags)
-
- // Url
- inputUrl.attachLiveData(this, viewModel.url)
- viewModel.onUrlWarningVisibility()
- .toViewVisibility(this, textUrlWarning)
-
- // Timeout
- responseTimeoutInput.attachLiveData(this, viewModel.timeout)
-
- // Validation mode
- responseValidationMode.attachLiveData(
- lifecycleOwner = this,
- data = viewModel.validationMode,
- outTransformer = { ValidationMode.fromIndex(it) },
- inTransformer = { it.toIndex() }
- )
- viewModel.onValidationModeDescription()
- .toViewText(this, validationModeDescription)
-
- // Validation search term
- responseValidationSearchTerm.attachLiveData(
- lifecycleOwner = this,
- data = viewModel.validationSearchTerm,
- pullInChanges = false
- )
- viewModel.onValidationSearchTermVisibility()
- .toViewVisibility(this, responseValidationSearchTerm)
-
- // SSL certificate
- sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it }
- viewModel.certificateUri.distinct()
- .observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) })
-
- // Headers
- headersLayout.attach(viewModel.headers)
- }
-
- private fun setupUi() {
- toolbarTitle.setText(R.string.add_site)
- toolbar.run {
- inflateMenu(R.menu.menu_addsite)
- setNavigationIcon(R.drawable.ic_action_close)
- setNavigationOnClickListener { finish() }
- }
-
- val validationOptionsAdapter = ArrayAdapter(
- this,
- R.layout.list_item_spinner,
- resources.getStringArray(R.array.response_validation_options)
- )
- validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
- responseValidationMode.adapter = validationOptionsAdapter
-
- scrollView.onScroll {
- appToolbar.elevation = if (it > appToolbar.measuredHeight / 2) {
- appToolbar.dimenFloat(R.dimen.default_elevation)
- } else {
- 0f
- }
- }
-
- // SSL certificate
- sslCertificateBrowse.setOnClickListener {
- val intent = Intent(ACTION_OPEN_DOCUMENT).apply {
- addCategory(CATEGORY_OPENABLE)
- type = "*/*"
- }
- startActivityForResult(intent, SELECT_CERT_FILE_RQ)
- }
- }
-
- private fun setupValidation() {
- validationForm = form {
- input(inputName, name = "Name") {
- isNotEmpty().description(R.string.please_enter_name)
- }
- input(inputUrl, name = "URL") {
- isNotEmpty().description(R.string.please_enter_url)
- isUrl().description(R.string.please_enter_valid_url)
- }
- input(responseTimeoutInput, name = "Timeout", optional = true) {
- isNumber().greaterThan(0)
- .description(R.string.please_enter_networkTimeout)
- }
- input(responseValidationSearchTerm, name = "Search term") {
- conditional(responseValidationSearchTerm.isVisibleCondition()) {
- isNotEmpty().description(R.string.please_enter_search_term)
- }
- }
- input(sslCertificateInput, name = "Certificate Path", optional = true) {
- isUri().hasScheme("file", "content")
- .that { it.host != null }
- .description(R.string.please_enter_validCertUri)
- }
- submitWith(toolbar.menu, R.id.commit) {
- viewModel.commit {
- setResult(RESULT_OK)
- finish()
- }
- }
- }
-
- // Validation script
- scriptInputLayout.attach(
- codeData = viewModel.validationScript,
- visibility = viewModel.onValidationScriptVisibility(),
- form = validationForm
- )
-
- // Check interval
- checkIntervalLayout.attach(
- valueData = viewModel.checkIntervalValue,
- multiplierData = viewModel.checkIntervalUnit,
- form = validationForm
- )
-
- // Retry Policy
- retryPolicyLayout.attach(
- timesData = viewModel.retryPolicyTimes,
- minutesData = viewModel.retryPolicyMinutes,
- form = validationForm
- )
- }
-
- override fun onResume() {
- super.onResume()
- appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {
- appToolbar.dimenFloat(R.dimen.default_elevation)
- } else {
- 0f
- }
- }
-
- override fun onActivityResult(
- requestCode: Int,
- resultCode: Int,
- resultData: Intent?
- ) {
- super.onActivityResult(requestCode, resultCode, resultData)
- if (requestCode == SELECT_CERT_FILE_RQ && resultCode == RESULT_OK) {
- sslCertificateInput.setText(resultData?.data?.toString() ?: "")
- }
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteViewModel.kt b/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteViewModel.kt
deleted file mode 100644
index d7d8ed5..0000000
--- a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteViewModel.kt
+++ /dev/null
@@ -1,187 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.ui.addsite
-
-import androidx.annotation.CheckResult
-import androidx.annotation.VisibleForTesting
-import androidx.annotation.VisibleForTesting.PRIVATE
-import androidx.lifecycle.Lifecycle.Event.ON_START
-import androidx.lifecycle.LifecycleObserver
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.OnLifecycleEvent
-import com.afollestad.nocknock.R
-import com.afollestad.nocknock.data.AppDatabase
-import com.afollestad.nocknock.data.model.Header
-import com.afollestad.nocknock.data.model.RetryPolicy
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.data.model.SiteSettings
-import com.afollestad.nocknock.data.model.Status.WAITING
-import com.afollestad.nocknock.data.model.ValidationMode
-import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
-import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
-import com.afollestad.nocknock.data.model.ValidationResult
-import com.afollestad.nocknock.data.putSite
-import com.afollestad.nocknock.engine.validation.ValidationExecutor
-import com.afollestad.nocknock.ui.ScopedViewModel
-import com.afollestad.nocknock.utilities.ext.MINUTE
-import com.afollestad.nocknock.utilities.livedata.map
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import okhttp3.HttpUrl
-import java.lang.System.currentTimeMillis
-
-/** @author Aidan Follestad (@afollestad) */
-class AddSiteViewModel(
- private val database: AppDatabase,
- private val validationManager: ValidationExecutor,
- mainDispatcher: CoroutineDispatcher,
- private val ioDispatcher: CoroutineDispatcher
-) : ScopedViewModel(mainDispatcher), LifecycleObserver {
-
- // Public properties
- val name = MutableLiveData()
- val tags = MutableLiveData()
- val url = MutableLiveData()
- val timeout = MutableLiveData()
- val validationMode = MutableLiveData()
- val validationSearchTerm = MutableLiveData()
- val validationScript = MutableLiveData()
- val checkIntervalValue = MutableLiveData()
- val checkIntervalUnit = MutableLiveData()
- val retryPolicyTimes = MutableLiveData()
- val retryPolicyMinutes = MutableLiveData()
- val headers = MutableLiveData>()
- val certificateUri = MutableLiveData()
-
- @OnLifecycleEvent(ON_START)
- fun setDefaults() {
- timeout.value = 10000
- validationMode.value = STATUS_CODE
- checkIntervalValue.value = 0
- checkIntervalUnit.value = MINUTE
- retryPolicyMinutes.value = 0
- retryPolicyMinutes.value = 0
- tags.value = ""
- headers.value = emptyList()
- }
-
- private val isLoading = MutableLiveData()
-
- @CheckResult fun onIsLoading(): LiveData = isLoading
-
- @CheckResult fun onUrlWarningVisibility(): LiveData {
- return url.map {
- val parsed = HttpUrl.parse(it)
- return@map it.isNotEmpty() && parsed == null
- }
- }
-
- @CheckResult fun onValidationModeDescription(): LiveData {
- return validationMode.map {
- when (it!!) {
- STATUS_CODE -> R.string.validation_mode_status_desc
- TERM_SEARCH -> R.string.validation_mode_term_desc
- JAVASCRIPT -> R.string.validation_mode_javascript_desc
- }
- }
- }
-
- @CheckResult fun onValidationSearchTermVisibility() = validationMode.map { it == TERM_SEARCH }
-
- @CheckResult fun onValidationScriptVisibility() = validationMode.map { it == JAVASCRIPT }
-
- // Actions
- fun commit(done: () -> Unit) {
- scope.launch {
- val newModel = generateDbModel() ?: return@launch
- isLoading.value = true
-
- val storedModel = withContext(ioDispatcher) {
- database.putSite(newModel)
- }
- validationManager.scheduleValidation(
- site = storedModel,
- rightNow = true,
- cancelPrevious = true
- )
-
- isLoading.value = false
- done()
- }
- }
-
- // Utilities
- @VisibleForTesting(otherwise = PRIVATE)
- fun getCheckIntervalMs(): Long {
- val value = checkIntervalValue.value ?: return 0
- val unit = checkIntervalUnit.value ?: return 0
- return value * unit
- }
-
- @VisibleForTesting(otherwise = PRIVATE)
- fun getValidationArgs(): String? {
- return when (validationMode.value) {
- TERM_SEARCH -> validationSearchTerm.value
- JAVASCRIPT -> validationScript.value
- else -> null
- }
- }
-
- private fun generateDbModel(): Site? {
- val timeout = timeout.value ?: 10_000
- val cleanedTags = tags.value?.split(',')?.joinToString { it.trim() } ?: ""
-
- val newSettings = SiteSettings(
- validationIntervalMs = getCheckIntervalMs(),
- validationMode = validationMode.value!!,
- validationArgs = getValidationArgs(),
- networkTimeout = timeout,
- disabled = false,
- certificate = certificateUri.value?.toString()
- )
-
- val newLastResult = ValidationResult(
- timestampMs = currentTimeMillis(),
- status = WAITING,
- reason = null
- )
-
- val retryPolicyTimes = retryPolicyTimes.value ?: 0
- val retryPolicyMinutes = retryPolicyMinutes.value ?: 0
- val newRetryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) {
- RetryPolicy(
- count = retryPolicyTimes,
- minutes = retryPolicyMinutes
- )
- } else {
- null
- }
-
- return Site(
- id = 0,
- name = name.value!!.trim(),
- url = url.value!!.trim(),
- tags = cleanedTags,
- settings = newSettings,
- lastResult = newLastResult,
- retryPolicy = newRetryPolicy,
- headers = headers.value ?: emptyList()
- )
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteViewModelExt.kt b/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteViewModelExt.kt
deleted file mode 100644
index c524555..0000000
--- a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteViewModelExt.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.ui.addsite
-
-import com.afollestad.nocknock.data.model.RetryPolicy
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
-import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
-import com.afollestad.nocknock.utilities.ext.DAY
-import com.afollestad.nocknock.utilities.ext.HOUR
-import com.afollestad.nocknock.utilities.ext.MINUTE
-import com.afollestad.nocknock.utilities.ext.WEEK
-import kotlin.math.ceil
-
-fun AddSiteViewModel.prePopulateFromModel(site: Site) {
- val settings = site.settings ?: throw IllegalArgumentException("Settings must be populated!")
-
- name.value = site.name
- tags.value = site.tags
- url.value = site.url
- timeout.value = settings.networkTimeout
-
- validationMode.value = settings.validationMode
- when (settings.validationMode) {
- TERM_SEARCH -> {
- validationSearchTerm.value = settings.validationArgs
- validationScript.value = null
- }
- JAVASCRIPT -> {
- validationSearchTerm.value = null
- validationScript.value = settings.validationArgs
- }
- else -> {
- validationSearchTerm.value = null
- validationScript.value = null
- }
- }
-
- setCheckInterval(settings.validationIntervalMs)
- setRetryPolicy(site.retryPolicy)
- headers.value = site.headers
-}
-
-private fun AddSiteViewModel.setCheckInterval(interval: Long) {
- when {
- interval >= WEEK -> {
- checkIntervalValue.value =
- getIntervalFromUnit(interval, WEEK)
- checkIntervalUnit.value = WEEK
- }
- interval >= DAY -> {
- checkIntervalValue.value =
- getIntervalFromUnit(interval, DAY)
- checkIntervalUnit.value = DAY
- }
- interval >= HOUR -> {
- checkIntervalValue.value =
- getIntervalFromUnit(interval, HOUR)
- checkIntervalUnit.value = HOUR
- }
- interval >= MINUTE -> {
- checkIntervalValue.value =
- getIntervalFromUnit(interval, MINUTE)
- checkIntervalUnit.value = MINUTE
- }
- else -> {
- checkIntervalValue.value = 0
- checkIntervalUnit.value = MINUTE
- }
- }
-}
-
-private fun AddSiteViewModel.setRetryPolicy(policy: RetryPolicy?) {
- if (policy == null) return
- retryPolicyTimes.value = policy.count
- retryPolicyMinutes.value = policy.minutes
-}
-
-private fun getIntervalFromUnit(
- millis: Long,
- unit: Long
-): Int {
- val intervalFloat = millis.toFloat()
- val byFloat = unit.toFloat()
- return ceil(intervalFloat / byFloat).toInt()
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivity.kt b/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivity.kt
deleted file mode 100644
index aec76d8..0000000
--- a/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivity.kt
+++ /dev/null
@@ -1,149 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.ui.main
-
-import android.content.Intent
-import android.os.Bundle
-import androidx.lifecycle.Observer
-import androidx.recyclerview.widget.DividerItemDecoration
-import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL
-import com.afollestad.materialdialogs.MaterialDialog
-import com.afollestad.materialdialogs.list.listItems
-import com.afollestad.nocknock.R
-import com.afollestad.nocknock.adapter.SiteAdapter
-import com.afollestad.nocknock.adapter.TagAdapter
-import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.dialogs.AboutDialog
-import com.afollestad.nocknock.notifications.NockNotificationManager
-import com.afollestad.nocknock.ui.DarkModeSwitchActivity
-import com.afollestad.nocknock.ui.NightMode.UNKNOWN
-import com.afollestad.nocknock.utilities.providers.IntentProvider
-import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
-import kotlinx.android.synthetic.main.activity_main.fab
-import kotlinx.android.synthetic.main.activity_main.list
-import kotlinx.android.synthetic.main.activity_main.loadingProgress
-import kotlinx.android.synthetic.main.include_app_bar.toolbar
-import kotlinx.android.synthetic.main.include_empty_view.emptyText
-import org.koin.android.ext.android.inject
-import org.koin.androidx.viewmodel.ext.android.viewModel
-import kotlinx.android.synthetic.main.activity_main.tags_list as tagsList
-
-/** @author Aidan Follestad (@afollestad) */
-class MainActivity : DarkModeSwitchActivity() {
-
- private val notificationManager by inject()
- private val intentProvider by inject()
-
- internal val viewModel by viewModel()
-
- private lateinit var siteAdapter: SiteAdapter
- private lateinit var tagAdapter: TagAdapter
-
- private val statusUpdateReceiver by lazy {
- StatusUpdateIntentReceiver(application, intentProvider) {
- viewModel.postSiteUpdate(it)
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- setupUi()
-
- notificationManager.createChannels()
-
- lifecycle.run {
- addObserver(viewModel)
- addObserver(statusUpdateReceiver)
- }
-
- viewModel.onSites()
- .observe(this, Observer { siteAdapter.set(it) })
- viewModel.onEmptyTextVisibility()
- .toViewVisibility(this, emptyText)
- viewModel.onTags()
- .observe(this, Observer { tagAdapter.set(it) })
- viewModel.onTagsListVisibility()
- .toViewVisibility(this, tagsList)
- loadingProgress.observe(this, viewModel.onIsLoading())
-
- processIntent(intent)
- }
-
- private fun setupUi() {
- toolbar.run {
- inflateMenu(R.menu.menu_main)
- menu.findItem(R.id.dark_mode)
- .apply {
- if (getCurrentNightMode() == UNKNOWN) {
- isChecked = isDarkMode()
- } else {
- isVisible = false
- }
- }
- setOnMenuItemClickListener { item ->
- when (item.itemId) {
- R.id.about -> AboutDialog.show(this@MainActivity)
- R.id.dark_mode -> toggleDarkMode()
- }
- return@setOnMenuItemClickListener true
- }
- }
-
- siteAdapter = SiteAdapter(this::onSiteSelected)
- list.run {
- layoutManager = LinearLayoutManager(this@MainActivity)
- adapter = siteAdapter
- addItemDecoration(DividerItemDecoration(this@MainActivity, VERTICAL))
- }
-
- tagAdapter = TagAdapter(viewModel::onTagSelection)
- tagsList.run {
- layoutManager = LinearLayoutManager(this@MainActivity, HORIZONTAL, false)
- adapter = tagAdapter
- }
-
- fab.setOnClickListener { addSite() }
- }
-
- override fun onNewIntent(intent: Intent?) {
- super.onNewIntent(intent)
- intent?.let(::processIntent)
- }
-
- private fun onSiteSelected(
- model: Site,
- longClick: Boolean
- ) {
- if (longClick) {
- MaterialDialog(this).show {
- title(R.string.options)
- listItems(R.array.site_long_options) { _, i, _ ->
- when (i) {
- 0 -> viewModel.refreshSite(model)
- 1 -> addSiteForDuplication(model)
- 2 -> maybeRemoveSite(model)
- }
- }
- }
- } else {
- viewSite(model)
- }
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivityExt.kt b/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivityExt.kt
deleted file mode 100644
index e11ca08..0000000
--- a/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivityExt.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.ui.main
-
-import android.content.Intent
-import com.afollestad.materialdialogs.MaterialDialog
-import com.afollestad.nocknock.R
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.toHtml
-import com.afollestad.nocknock.ui.addsite.AddSiteActivity
-import com.afollestad.nocknock.ui.viewsite.KEY_SITE
-import com.afollestad.nocknock.ui.viewsite.ViewSiteActivity
-import com.afollestad.nocknock.utilities.providers.RealIntentProvider.Companion.KEY_VIEW_NOTIFICATION_MODEL
-
-internal const val VIEW_SITE_RQ = 6923
-internal const val ADD_SITE_RQ = 6969
-
-// ADD
-
-internal fun MainActivity.addSite() {
- startActivityForResult(intentToAdd(), ADD_SITE_RQ)
-}
-
-internal fun MainActivity.addSiteForDuplication(site: Site) {
- startActivityForResult(intentToAdd(site), ADD_SITE_RQ)
-}
-
-private fun MainActivity.intentToAdd(model: Site? = null) =
- Intent(this, AddSiteActivity::class.java).apply {
- model?.let { putExtra(KEY_SITE, it) }
- }
-
-// VIEW
-
-internal fun MainActivity.viewSite(model: Site) {
- startActivityForResult(intentToView(model), VIEW_SITE_RQ)
-}
-
-private fun MainActivity.intentToView(model: Site) =
- Intent(this, ViewSiteActivity::class.java).apply {
- putExtra(KEY_SITE, model)
- }
-
-// MISC
-
-internal fun MainActivity.maybeRemoveSite(model: Site) {
- MaterialDialog(this).show {
- title(R.string.remove_site)
- message(text = context.getString(R.string.remove_site_prompt, model.name).toHtml())
- positiveButton(R.string.remove) { viewModel.removeSite(model) }
- negativeButton(android.R.string.cancel)
- }
-}
-
-internal fun MainActivity.processIntent(intent: Intent) {
- if (intent.hasExtra(KEY_VIEW_NOTIFICATION_MODEL)) {
- val model = intent.getSerializableExtra(KEY_VIEW_NOTIFICATION_MODEL) as Site
- viewSite(model)
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/main/MainViewModel.kt b/app/src/main/java/com/afollestad/nocknock/ui/main/MainViewModel.kt
deleted file mode 100644
index bad64fc..0000000
--- a/app/src/main/java/com/afollestad/nocknock/ui/main/MainViewModel.kt
+++ /dev/null
@@ -1,157 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.ui.main
-
-import androidx.annotation.CheckResult
-import androidx.lifecycle.Lifecycle.Event.ON_RESUME
-import androidx.lifecycle.LifecycleObserver
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.OnLifecycleEvent
-import com.afollestad.nocknock.data.AppDatabase
-import com.afollestad.nocknock.data.allSites
-import com.afollestad.nocknock.data.deleteSite
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.engine.validation.ValidationExecutor
-import com.afollestad.nocknock.notifications.NockNotificationManager
-import com.afollestad.nocknock.ui.ScopedViewModel
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-/** @author Aidan Follestad (@afollestad) */
-class MainViewModel(
- private val database: AppDatabase,
- private val notificationManager: NockNotificationManager,
- private val validationManager: ValidationExecutor,
- mainDispatcher: CoroutineDispatcher,
- private val ioDispatcher: CoroutineDispatcher
-) : ScopedViewModel(mainDispatcher), LifecycleObserver {
-
- private val sites = MutableLiveData>()
- private val isLoading = MutableLiveData()
- private val emptyTextVisibility = MutableLiveData()
- private val tags = MutableLiveData>()
- private val tagsListVisibility = MutableLiveData()
-
- @CheckResult fun onSites(): LiveData> = sites
-
- @CheckResult fun onIsLoading(): LiveData = isLoading
-
- @CheckResult fun onEmptyTextVisibility(): LiveData = emptyTextVisibility
-
- @CheckResult fun onTags(): LiveData> = tags
-
- @CheckResult fun onTagsListVisibility(): LiveData = tagsListVisibility
-
- @OnLifecycleEvent(ON_RESUME)
- fun onResume() = loadSites(emptyList())
-
- fun onTagSelection(tags: List) = loadSites(tags)
-
- fun postSiteUpdate(model: Site) {
- val currentSites = sites.value ?: return
- val index = currentSites.indexOfFirst { it.id == model.id }
- if (index == -1) return
- sites.value = currentSites.toMutableList()
- .apply {
- this[index] = model
- }
- }
-
- fun refreshSite(model: Site) {
- validationManager.scheduleValidation(
- site = model,
- rightNow = true,
- cancelPrevious = true
- )
- }
-
- fun removeSite(model: Site) {
- validationManager.cancelScheduledValidation(model)
- notificationManager.cancelStatusNotification(model)
-
- scope.launch {
- isLoading.value = true
- withContext(ioDispatcher) { database.deleteSite(model) }
-
- val currentSites = sites.value ?: return@launch
- val index = currentSites.indexOfFirst { it.id == model.id }
- isLoading.value = false
- if (index == -1) return@launch
-
- val newSitesList = currentSites.toMutableList()
- .apply {
- removeAt(index)
- }
- sites.value = newSitesList
- emptyTextVisibility.value = newSitesList.isEmpty()
- }
- }
-
- private fun loadSites(forTags: List) {
- scope.launch {
- notificationManager.cancelStatusNotifications()
- emptyTextVisibility.value = false
- isLoading.value = true
-
- val unfiltered = withContext(ioDispatcher) {
- database.allSites()
- }
- var result = unfiltered
-
- if (forTags.isNotEmpty()) {
- result = result.filter { site ->
- val itemTags = site.tags.toLowerCase()
- .split(",")
- itemTags.any { tag -> forTags.contains(tag) }
- }
- }
-
- sites.value = result
- ensureCheckJobs()
- isLoading.value = false
- emptyTextVisibility.value = result.isEmpty()
-
- val tagsValues = pullOutTags(unfiltered)
- tags.value = tagsValues
- tagsListVisibility.value = tagsValues.isNotEmpty()
- }
- }
-
- private suspend fun ensureCheckJobs() {
- withContext(ioDispatcher) {
- validationManager.ensureScheduledValidations()
- }
- }
-
- private fun pullOutTags(sites: List): List {
- return mutableListOf().apply {
- for (site in sites) {
- val splitTags = site.tags.toLowerCase()
- .split(',')
- splitTags
- .filter { it.isNotEmpty() }
- .forEach { tag ->
- if (!this.contains(tag)) {
- this.add(tag)
- }
- }
- }
- sort()
- }
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt
deleted file mode 100644
index 2aa312c..0000000
--- a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt
+++ /dev/null
@@ -1,291 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.ui.viewsite
-
-import android.annotation.SuppressLint
-import android.content.Intent
-import android.content.Intent.ACTION_OPEN_DOCUMENT
-import android.content.Intent.CATEGORY_OPENABLE
-import android.os.Bundle
-import android.widget.ArrayAdapter
-import androidx.lifecycle.Observer
-import com.afollestad.nocknock.R
-import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.data.model.ValidationMode
-import com.afollestad.nocknock.ui.DarkModeSwitchActivity
-import com.afollestad.nocknock.utilities.ext.onTextChanged
-import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection
-import com.afollestad.nocknock.utilities.livedata.distinct
-import com.afollestad.nocknock.utilities.providers.IntentProvider
-import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
-import com.afollestad.nocknock.viewcomponents.ext.isVisibleCondition
-import com.afollestad.nocknock.viewcomponents.ext.onScroll
-import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
-import com.afollestad.nocknock.viewcomponents.livedata.toViewText
-import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
-import com.afollestad.vvalidator.form
-import com.afollestad.vvalidator.form.Form
-import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalLayout
-import kotlinx.android.synthetic.main.activity_viewsite.headersLayout
-import kotlinx.android.synthetic.main.activity_viewsite.iconStatus
-import kotlinx.android.synthetic.main.activity_viewsite.inputName
-import kotlinx.android.synthetic.main.activity_viewsite.inputTags
-import kotlinx.android.synthetic.main.activity_viewsite.inputUrl
-import kotlinx.android.synthetic.main.activity_viewsite.loadingProgress
-import kotlinx.android.synthetic.main.activity_viewsite.responseTimeoutInput
-import kotlinx.android.synthetic.main.activity_viewsite.responseValidationMode
-import kotlinx.android.synthetic.main.activity_viewsite.responseValidationSearchTerm
-import kotlinx.android.synthetic.main.activity_viewsite.retryPolicyLayout
-import kotlinx.android.synthetic.main.activity_viewsite.scriptInputLayout
-import kotlinx.android.synthetic.main.activity_viewsite.scrollView
-import kotlinx.android.synthetic.main.activity_viewsite.sslCertificateBrowse
-import kotlinx.android.synthetic.main.activity_viewsite.sslCertificateInput
-import kotlinx.android.synthetic.main.activity_viewsite.textLastCheckResult
-import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck
-import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
-import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription
-import kotlinx.android.synthetic.main.include_app_bar.toolbar
-import org.koin.android.ext.android.inject
-import org.koin.androidx.viewmodel.ext.android.viewModel
-import kotlinx.android.synthetic.main.include_app_bar.app_toolbar as appToolbar
-import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
-
-/** @author Aidan Follestad (@afollestad) */
-class ViewSiteActivity : DarkModeSwitchActivity() {
- companion object {
- private const val SELECT_CERT_FILE_RQ = 23
- }
-
- internal val viewModel by viewModel()
- private lateinit var validationForm: Form
-
- private val intentProvider by inject()
- private val statusUpdateReceiver by lazy {
- StatusUpdateIntentReceiver(application, intentProvider) {
- viewModel.setModel(it)
- }
- }
-
- @SuppressLint("SetTextI18n")
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_viewsite)
-
- // Populate view model with initial data
- val model = intent.getSerializableExtra(KEY_SITE) as Site
- viewModel.setModel(model)
-
- setupUi()
- setupValidation()
- lifecycle.run {
- addObserver(viewModel)
- addObserver(statusUpdateReceiver)
- }
-
- // Loading
- loadingProgress.observe(this, viewModel.onIsLoading())
-
- // Status
- viewModel.status.observe(this, Observer {
- iconStatus.setStatus(it)
- invalidateMenuForStatus(it)
- })
-
- // Name
- inputName.attachLiveData(this, viewModel.name)
-
- // Tags
- inputTags.attachLiveData(this, viewModel.tags)
-
- // Url
- inputUrl.attachLiveData(this, viewModel.url)
- viewModel.onUrlWarningVisibility()
- .toViewVisibility(this, textUrlWarning)
-
- // Timeout
- responseTimeoutInput.attachLiveData(this, viewModel.timeout)
-
- // Validation mode
- responseValidationMode.attachLiveData(
- lifecycleOwner = this,
- data = viewModel.validationMode,
- outTransformer = { ValidationMode.fromIndex(it) },
- inTransformer = { it.toIndex() }
- )
- viewModel.onValidationModeDescription()
- .toViewText(this, validationModeDescription)
-
- // Validation search term
- responseValidationSearchTerm.attachLiveData(this, viewModel.validationSearchTerm)
- viewModel.onValidationSearchTermVisibility()
- .toViewVisibility(this, responseValidationSearchTerm)
-
- // SSL certificate
- sslCertificateInput.onTextChanged { viewModel.certificateUri.value = it }
- viewModel.certificateUri.distinct()
- .observe(this, Observer { sslCertificateInput.setTextAndMaintainSelection(it) })
-
- // Headers
- headersLayout.attach(viewModel.headers)
-
- // Last/next check
- viewModel.onLastCheckResultText()
- .toViewText(this, textLastCheckResult)
- viewModel.onNextCheckText()
- .toViewText(this, textNextCheck)
- }
-
- private fun setupUi() {
- toolbarTitle.text = ""
- toolbar.run {
- setNavigationIcon(R.drawable.ic_action_close)
- setNavigationOnClickListener { finish() }
- inflateMenu(R.menu.menu_viewsite)
-
- menu.findItem(R.id.refresh)
- .setActionView(R.layout.menu_item_refresh_icon)
- .apply {
- actionView.setOnClickListener { viewModel.checkNow() }
- }
-
- setOnMenuItemClickListener {
- when (it.itemId) {
- R.id.remove -> maybeRemoveSite()
- R.id.disableChecks -> maybeDisableChecks()
- }
- true
- }
- }
-
- scrollView.onScroll {
- appToolbar.elevation = if (it > appToolbar.measuredHeight / 2) {
- appToolbar.dimenFloat(R.dimen.default_elevation)
- } else {
- 0f
- }
- }
-
- val validationOptionsAdapter = ArrayAdapter(
- this,
- R.layout.list_item_spinner,
- resources.getStringArray(R.array.response_validation_options)
- )
- validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
- responseValidationMode.adapter = validationOptionsAdapter
-
- // Disabled button
- viewModel.onDisableChecksVisibility()
- .observe(this, Observer {
- toolbar.menu.findItem(R.id.disableChecks)
- .isVisible = it
- })
-
- // Done item text
- viewModel.onDoneButtonText()
- .observe(this, Observer {
- toolbar.menu.findItem(R.id.commit)
- .setTitle(it)
- })
-
- // SSL certificate
- sslCertificateBrowse.setOnClickListener {
- val intent = Intent(ACTION_OPEN_DOCUMENT).apply {
- addCategory(CATEGORY_OPENABLE)
- type = "*/*"
- }
- startActivityForResult(intent, SELECT_CERT_FILE_RQ)
- }
- }
-
- private fun setupValidation() {
- validationForm = form {
- input(inputName, name = "Name") {
- isNotEmpty().description(R.string.please_enter_name)
- }
- input(inputUrl, name = "URL") {
- isNotEmpty().description(R.string.please_enter_url)
- isUrl().description(R.string.please_enter_valid_url)
- }
- input(responseValidationSearchTerm, name = "Search term") {
- conditional(responseValidationSearchTerm.isVisibleCondition()) {
- isNotEmpty().description(R.string.please_enter_search_term)
- }
- }
- input(responseTimeoutInput, name = "Timeout", optional = true) {
- isNumber().greaterThan(0)
- .description(R.string.please_enter_networkTimeout)
- }
- input(sslCertificateInput, name = "Certificate Path", optional = true) {
- isUri().hasScheme("file", "content")
- .that { it.host != null }
- .description(R.string.please_enter_validCertUri)
- }
- submitWith(toolbar.menu, R.id.commit) {
- viewModel.commit { finish() }
- }
- }
-
- // Validation script
- scriptInputLayout.attach(
- codeData = viewModel.validationScript,
- visibility = viewModel.onValidationScriptVisibility(),
- form = validationForm
- )
-
- // Check interval
- checkIntervalLayout.attach(
- valueData = viewModel.checkIntervalValue,
- multiplierData = viewModel.checkIntervalUnit,
- form = validationForm
- )
-
- // Retry Policy
- retryPolicyLayout.attach(
- timesData = viewModel.retryPolicyTimes,
- minutesData = viewModel.retryPolicyMinutes,
- form = validationForm
- )
- }
-
- override fun onResume() {
- super.onResume()
- appToolbar.elevation = if (scrollView.scrollY > appToolbar.measuredHeight / 2) {
- appToolbar.dimenFloat(R.dimen.default_elevation)
- } else {
- 0f
- }
- }
-
- override fun onActivityResult(
- requestCode: Int,
- resultCode: Int,
- resultData: Intent?
- ) {
- super.onActivityResult(requestCode, resultCode, resultData)
- if (requestCode == SELECT_CERT_FILE_RQ && resultCode == RESULT_OK) {
- sslCertificateInput.setText(resultData?.data?.toString() ?: "")
- }
- }
-
- override fun onNewIntent(intent: Intent?) {
- super.onNewIntent(intent)
- if (intent != null && intent.hasExtra(KEY_SITE)) {
- val newModel = intent.getSerializableExtra(KEY_SITE) as Site
- viewModel.setModel(newModel)
- }
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivityExt.kt b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivityExt.kt
deleted file mode 100644
index ab0a6ea..0000000
--- a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivityExt.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.ui.viewsite
-
-import android.widget.ImageView
-import com.afollestad.materialdialogs.MaterialDialog
-import com.afollestad.nocknock.R
-import com.afollestad.nocknock.data.model.Status
-import com.afollestad.nocknock.data.model.isPending
-import com.afollestad.nocknock.toHtml
-import com.afollestad.nocknock.utilities.ext.animateRotation
-import kotlinx.android.synthetic.main.include_app_bar.toolbar
-
-const val KEY_SITE = "site_model"
-
-internal fun ViewSiteActivity.maybeRemoveSite() {
- val model = viewModel.site
- MaterialDialog(this).show {
- title(R.string.remove_site)
- message(text = context.getString(R.string.remove_site_prompt, model.name).toHtml())
- positiveButton(R.string.remove) {
- viewModel.removeSite { finish() }
- }
- negativeButton(android.R.string.cancel)
- }
-}
-
-internal fun ViewSiteActivity.maybeDisableChecks() {
- val model = viewModel.site
- MaterialDialog(this).show {
- title(R.string.disable_automatic_checks)
- message(
- text = context.getString(R.string.disable_automatic_checks_prompt, model.name).toHtml()
- )
- positiveButton(R.string.disable) { viewModel.disableSite() }
- negativeButton(android.R.string.cancel)
- }
-}
-
-internal fun ViewSiteActivity.invalidateMenuForStatus(status: Status) {
- val refreshIcon = toolbar.menu.findItem(R.id.refresh)
- .actionView as ImageView
- if (status.isPending()) {
- refreshIcon.animateRotation()
- } else {
- refreshIcon.run {
- animate().cancel()
- rotation = 0f
- }
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModel.kt b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModel.kt
deleted file mode 100644
index b5c9f93..0000000
--- a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModel.kt
+++ /dev/null
@@ -1,268 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.ui.viewsite
-
-import androidx.annotation.CheckResult
-import androidx.annotation.VisibleForTesting
-import androidx.annotation.VisibleForTesting.PRIVATE
-import androidx.lifecycle.LifecycleObserver
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import com.afollestad.nocknock.R
-import com.afollestad.nocknock.data.AppDatabase
-import com.afollestad.nocknock.data.deleteSite
-import com.afollestad.nocknock.data.model.Header
-import com.afollestad.nocknock.data.model.RetryPolicy
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.data.model.Status
-import com.afollestad.nocknock.data.model.Status.WAITING
-import com.afollestad.nocknock.data.model.ValidationMode
-import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
-import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
-import com.afollestad.nocknock.data.model.ValidationResult
-import com.afollestad.nocknock.data.model.textRes
-import com.afollestad.nocknock.data.updateSite
-import com.afollestad.nocknock.engine.validation.ValidationExecutor
-import com.afollestad.nocknock.notifications.NockNotificationManager
-import com.afollestad.nocknock.ui.ScopedViewModel
-import com.afollestad.nocknock.utilities.ext.formatDate
-import com.afollestad.nocknock.utilities.livedata.map
-import com.afollestad.nocknock.utilities.livedata.zip
-import com.afollestad.nocknock.utilities.providers.StringProvider
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import okhttp3.HttpUrl
-import java.lang.System.currentTimeMillis
-
-/** @author Aidan Follestad (@afollestad) */
-class ViewSiteViewModel(
- private val stringProvider: StringProvider,
- private val database: AppDatabase,
- private val notificationManager: NockNotificationManager,
- private val validationManager: ValidationExecutor,
- mainDispatcher: CoroutineDispatcher,
- private val ioDispatcher: CoroutineDispatcher
-) : ScopedViewModel(mainDispatcher), LifecycleObserver {
-
- lateinit var site: Site
-
- // Public properties
- val status = MutableLiveData()
- val name = MutableLiveData()
- val tags = MutableLiveData()
- val url = MutableLiveData()
- val timeout = MutableLiveData()
- val validationMode = MutableLiveData()
- val validationSearchTerm = MutableLiveData()
- val validationScript = MutableLiveData()
- val checkIntervalValue = MutableLiveData()
- val checkIntervalUnit = MutableLiveData()
- val retryPolicyTimes = MutableLiveData()
- val retryPolicyMinutes = MutableLiveData()
- val headers = MutableLiveData>()
- val certificateUri = MutableLiveData()
- internal val disabled = MutableLiveData()
- internal val lastResult = MutableLiveData()
-
- private val isLoading = MutableLiveData()
-
- @CheckResult fun onIsLoading(): LiveData = isLoading
-
- @CheckResult fun onUrlWarningVisibility(): LiveData {
- return url.map {
- val parsed = HttpUrl.parse(it)
- return@map it.isNotEmpty() && parsed == null
- }
- }
-
- @CheckResult fun onValidationModeDescription(): LiveData {
- return validationMode.map {
- when (it!!) {
- STATUS_CODE -> R.string.validation_mode_status_desc
- TERM_SEARCH -> R.string.validation_mode_term_desc
- JAVASCRIPT -> R.string.validation_mode_javascript_desc
- }
- }
- }
-
- @CheckResult fun onValidationSearchTermVisibility() = validationMode.map { it == TERM_SEARCH }
-
- @CheckResult fun onValidationScriptVisibility() = validationMode.map { it == JAVASCRIPT }
-
- @CheckResult fun onDisableChecksVisibility(): LiveData = disabled.map { !it }
-
- @CheckResult fun onDoneButtonText(): LiveData =
- disabled.map {
- if (it) R.string.renable_and_save_changes
- else R.string.save_changes
- }
-
- @CheckResult fun onLastCheckResultText(): LiveData = lastResult.map {
- if (it == null) {
- stringProvider.get(R.string.none)
- } else {
- val statusText = it.status.textRes()
- if (statusText == 0) {
- it.reason
- } else {
- stringProvider.get(statusText)
- }
- }
- }
-
- @CheckResult fun onNextCheckText(): LiveData {
- return zip(disabled, lastResult)
- .map {
- val disabled = it.first
- val lastResult = it.second
- if (disabled) {
- stringProvider.get(R.string.auto_checks_disabled)
- } else {
- val lastCheck = lastResult?.timestampMs ?: currentTimeMillis()
- (lastCheck + getCheckIntervalMs()).formatDate()
- }
- }
- }
-
- // Actions
- fun commit(done: () -> Unit) {
- scope.launch {
- val updatedModel = getUpdatedDbModel() ?: return@launch
- isLoading.value = true
-
- withContext(ioDispatcher) {
- database.updateSite(updatedModel)
- }
- validationManager.scheduleValidation(
- site = updatedModel,
- rightNow = true,
- cancelPrevious = true
- )
-
- isLoading.value = false
- done()
- }
- }
-
- fun checkNow() {
- val checkModel = site.withStatus(
- status = WAITING
- )
- setModel(checkModel)
- validationManager.scheduleValidation(
- site = checkModel,
- rightNow = true,
- cancelPrevious = true
- )
- }
-
- fun removeSite(done: () -> Unit) {
- validationManager.cancelScheduledValidation(site)
- notificationManager.cancelStatusNotification(site)
-
- scope.launch {
- isLoading.value = true
- withContext(ioDispatcher) {
- database.deleteSite(site)
- }
- isLoading.value = false
- done()
- }
- }
-
- fun disableSite() {
- validationManager.cancelScheduledValidation(site)
- notificationManager.cancelStatusNotification(site)
-
- scope.launch {
- isLoading.value = true
- val newModel = site.copy(
- settings = site.settings!!.copy(
- disabled = true
- )
- )
- withContext(ioDispatcher) {
- database.updateSite(newModel)
- }
- isLoading.value = false
- setModel(newModel)
- }
- }
-
- // Utilities
- @VisibleForTesting(otherwise = PRIVATE)
- fun getCheckIntervalMs(): Long {
- val value = checkIntervalValue.value ?: return 0
- val unit = checkIntervalUnit.value ?: return 0
- return value * unit
- }
-
- @VisibleForTesting(otherwise = PRIVATE)
- fun getValidationArgs(): String? {
- return when (validationMode.value) {
- TERM_SEARCH -> validationSearchTerm.value?.trim()
- JAVASCRIPT -> validationScript.value?.trim()
- else -> null
- }
- }
-
- private fun getUpdatedDbModel(): Site? {
- val timeout = timeout.value ?: 10_000
- val cleanedTags = tags.value?.split(',')?.joinToString(separator = ",") ?: ""
-
- val newSettings = site.settings!!.copy(
- validationIntervalMs = getCheckIntervalMs(),
- validationMode = validationMode.value!!,
- validationArgs = getValidationArgs(),
- networkTimeout = timeout,
- disabled = false,
- certificate = certificateUri.value?.toString()
- )
-
- val retryPolicyTimes = retryPolicyTimes.value ?: 0
- val retryPolicyMinutes = retryPolicyMinutes.value ?: 0
- val retryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) {
- if (site.retryPolicy != null) {
- // Have existing policy, update it
- site.retryPolicy!!.copy(
- count = retryPolicyTimes,
- minutes = retryPolicyMinutes
- )
- } else {
- // Create new policy
- RetryPolicy(
- count = retryPolicyTimes,
- minutes = retryPolicyMinutes
- )
- }
- } else {
- // No policy
- null
- }
-
- return site.copy(
- name = name.value!!.trim(),
- tags = cleanedTags,
- url = url.value!!.trim(),
- settings = newSettings,
- retryPolicy = retryPolicy,
- headers = headers.value ?: emptyList()
- )
- .withStatus(status = WAITING)
- }
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelExt.kt b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelExt.kt
deleted file mode 100644
index 800f235..0000000
--- a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelExt.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.ui.viewsite
-
-import com.afollestad.nocknock.data.model.RetryPolicy
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.data.model.Status.WAITING
-import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
-import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
-import com.afollestad.nocknock.utilities.ext.DAY
-import com.afollestad.nocknock.utilities.ext.HOUR
-import com.afollestad.nocknock.utilities.ext.MINUTE
-import com.afollestad.nocknock.utilities.ext.WEEK
-import kotlin.math.ceil
-
-fun ViewSiteViewModel.setModel(site: Site) {
- val settings = site.settings ?: throw IllegalArgumentException("Settings must be populated!")
- this.site = site
-
- status.value = site.lastResult?.status ?: WAITING
- name.value = site.name
- tags.value = site.tags
- url.value = site.url
- timeout.value = settings.networkTimeout
-
- validationMode.value = settings.validationMode
- when (settings.validationMode) {
- TERM_SEARCH -> {
- validationSearchTerm.value = settings.validationArgs
- validationScript.value = null
- }
- JAVASCRIPT -> {
- validationSearchTerm.value = null
- validationScript.value = settings.validationArgs
- }
- else -> {
- validationSearchTerm.value = null
- validationScript.value = null
- }
- }
-
- setCheckInterval(settings.validationIntervalMs)
- setRetryPolicy(site.retryPolicy)
- headers.value = site.headers
- if (settings.certificate == "null") {
- certificateUri.value = ""
- } else {
- certificateUri.value = settings.certificate
- }
-
- this.disabled.value = settings.disabled
- this.lastResult.value = site.lastResult
-}
-
-private fun ViewSiteViewModel.setCheckInterval(interval: Long) {
- when {
- interval >= WEEK -> {
- checkIntervalValue.value =
- getIntervalFromUnit(interval, WEEK)
- checkIntervalUnit.value = WEEK
- }
- interval >= DAY -> {
- checkIntervalValue.value =
- getIntervalFromUnit(interval, DAY)
- checkIntervalUnit.value = DAY
- }
- interval >= HOUR -> {
- checkIntervalValue.value =
- getIntervalFromUnit(interval, HOUR)
- checkIntervalUnit.value = HOUR
- }
- interval >= MINUTE -> {
- checkIntervalValue.value =
- getIntervalFromUnit(interval, MINUTE)
- checkIntervalUnit.value = MINUTE
- }
- else -> {
- checkIntervalValue.value = 0
- checkIntervalUnit.value = MINUTE
- }
- }
-}
-
-private fun ViewSiteViewModel.setRetryPolicy(policy: RetryPolicy?) {
- if (policy == null) return
- retryPolicyTimes.value = policy.count
- retryPolicyMinutes.value = policy.minutes
-}
-
-private fun getIntervalFromUnit(
- millis: Long,
- unit: Long
-): Int {
- val intervalFloat = millis.toFloat()
- val byFloat = unit.toFloat()
- return ceil(intervalFloat / byFloat).toInt()
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/util/AlarmUtil.java b/app/src/main/java/com/afollestad/nocknock/util/AlarmUtil.java
new file mode 100644
index 0000000..7effdb4
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/util/AlarmUtil.java
@@ -0,0 +1,59 @@
+package com.afollestad.nocknock.util;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import com.afollestad.nocknock.api.ServerModel;
+import com.afollestad.nocknock.services.CheckService;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * @author Aidan Follestad (afollestad)
+ */
+public class AlarmUtil {
+
+ private final static int BASE_RQC = 69;
+
+ public static PendingIntent getSiteIntent(Context context, ServerModel site) {
+ return PendingIntent.getService(context,
+ BASE_RQC + (int) site.id,
+ new Intent(context, CheckService.class)
+ .putExtra(CheckService.MODEL_ID, site.id),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ private static AlarmManager am(Context context) {
+ return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ }
+
+ public static void cancelSiteChecks(Context context, ServerModel site) {
+ PendingIntent pi = getSiteIntent(context, site);
+ am(context).cancel(pi);
+ }
+
+ public static void setSiteChecks(Context context, ServerModel site) {
+ cancelSiteChecks(context, site);
+ if (site.checkInterval <= 0) return;
+ if (site.lastCheck <= 0)
+ site.lastCheck = System.currentTimeMillis();
+ final long nextCheck = site.lastCheck + site.checkInterval;
+ final AlarmManager aMgr = am(context);
+ final PendingIntent serviceIntent = getSiteIntent(context, site);
+ aMgr.setRepeating(AlarmManager.RTC_WAKEUP, nextCheck, site.checkInterval, serviceIntent);
+ final SimpleDateFormat df = new SimpleDateFormat("EEE MMM dd hh:mm:ssa z yyyy", Locale.getDefault());
+ Log.d("AlarmUtil", String.format(Locale.getDefault(), "Set site check alarm for %s (%s), check interval: %d, next check: %s",
+ site.name, site.url, site.checkInterval, df.format(new Date(nextCheck))));
+ }
+
+ public static void setSiteChecks(Context context, ServerModel[] sites) {
+ if (sites == null || sites.length == 0) return;
+ for (ServerModel site : sites)
+ setSiteChecks(context, site);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/afollestad/nocknock/util/JsUtil.java b/app/src/main/java/com/afollestad/nocknock/util/JsUtil.java
new file mode 100644
index 0000000..160927b
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/util/JsUtil.java
@@ -0,0 +1,72 @@
+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() {
+ }
+}
diff --git a/app/src/main/java/com/afollestad/nocknock/util/MathUtil.java b/app/src/main/java/com/afollestad/nocknock/util/MathUtil.java
new file mode 100644
index 0000000..6cb885b
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/util/MathUtil.java
@@ -0,0 +1,32 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/afollestad/nocknock/util/NetworkUtil.java b/app/src/main/java/com/afollestad/nocknock/util/NetworkUtil.java
new file mode 100644
index 0000000..3680f26
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/util/NetworkUtil.java
@@ -0,0 +1,18 @@
+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();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/afollestad/nocknock/util/TimeUtil.java b/app/src/main/java/com/afollestad/nocknock/util/TimeUtil.java
new file mode 100644
index 0000000..3a65b73
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/util/TimeUtil.java
@@ -0,0 +1,32 @@
+package com.afollestad.nocknock.util;
+
+/**
+ * @author Aidan Follestad (afollestad)
+ */
+public class TimeUtil {
+
+ public final static long SECOND = 1000;
+ public final static long MINUTE = SECOND * 60;
+ public final static long HOUR = MINUTE * 60;
+ public final static long DAY = HOUR * 24;
+ public final static long WEEK = DAY * 7;
+ public final static long MONTH = WEEK * 4;
+
+ public static String str(long duration) {
+ if (duration <= 0) {
+ return "";
+ } else if (duration >= MONTH) {
+ return (int) Math.ceil(((float) duration / (float) MONTH)) + "mo";
+ } else if (duration >= WEEK) {
+ return (int) Math.ceil(((float) duration / (float) WEEK)) + "w";
+ } else if (duration >= DAY) {
+ return (int) Math.ceil(((float) duration / (float) DAY)) + "d";
+ } else if (duration >= HOUR) {
+ return (int) Math.ceil(((float) duration / (float) HOUR)) + "h";
+ } else if (duration >= MINUTE) {
+ return (int) Math.ceil(((float) duration / (float) MINUTE)) + "m";
+ } else {
+ return "<1m";
+ }
+ }
+}
diff --git a/app/src/main/java/com/afollestad/nocknock/views/DividerItemDecoration.java b/app/src/main/java/com/afollestad/nocknock/views/DividerItemDecoration.java
new file mode 100644
index 0000000..cc4396a
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/views/DividerItemDecoration.java
@@ -0,0 +1,104 @@
+package com.afollestad.nocknock.views;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+public class DividerItemDecoration extends RecyclerView.ItemDecoration {
+
+ private static final int[] ATTRS = new int[]{
+ android.R.attr.listDivider
+ };
+
+ public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
+
+ public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
+
+ private Drawable mDivider;
+
+ private int mOrientation;
+
+ public DividerItemDecoration(Context context, int orientation) {
+ final TypedArray a = context.obtainStyledAttributes(ATTRS);
+ mDivider = a.getDrawable(0);
+ a.recycle();
+ setOrientation(orientation);
+ }
+
+ public void setOrientation(int orientation) {
+ if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
+ throw new IllegalArgumentException("invalid orientation");
+ }
+ mOrientation = orientation;
+ }
+
+ @Override
+ public void onDraw(Canvas c, RecyclerView parent) {
+ if (mOrientation == VERTICAL_LIST) {
+ drawVertical(c, parent);
+ } else {
+ drawHorizontal(c, parent);
+ }
+ }
+
+ public void drawVertical(Canvas c, RecyclerView parent) {
+ final int left = parent.getPaddingLeft();
+ final int right = parent.getWidth() - parent.getPaddingRight();
+
+ final int childCount = parent.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = parent.getChildAt(i);
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
+ .getLayoutParams();
+ final int top = child.getBottom() + params.bottomMargin;
+ final int bottom = top + mDivider.getIntrinsicHeight();
+ mDivider.setBounds(left, top, right, bottom);
+ mDivider.draw(c);
+ }
+ }
+
+ public void drawHorizontal(Canvas c, RecyclerView parent) {
+ final int top = parent.getPaddingTop();
+ final int bottom = parent.getHeight() - parent.getPaddingBottom();
+
+ final int childCount = parent.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = parent.getChildAt(i);
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
+ .getLayoutParams();
+ final int left = child.getRight() + params.rightMargin;
+ final int right = left + mDivider.getIntrinsicHeight();
+ mDivider.setBounds(left, top, right, bottom);
+ mDivider.draw(c);
+ }
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
+ if (mOrientation == VERTICAL_LIST) {
+ outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
+ } else {
+ outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/afollestad/nocknock/views/StatusImageView.java b/app/src/main/java/com/afollestad/nocknock/views/StatusImageView.java
new file mode 100644
index 0000000..ac8cf13
--- /dev/null
+++ b/app/src/main/java/com/afollestad/nocknock/views/StatusImageView.java
@@ -0,0 +1,47 @@
+package com.afollestad.nocknock.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+import com.afollestad.nocknock.R;
+import com.afollestad.nocknock.api.ServerStatus;
+
+/**
+ * @author Aidan Follestad (afollestad)
+ */
+public class StatusImageView extends ImageView {
+
+ public StatusImageView(Context context) {
+ super(context);
+ setStatus(ServerStatus.OK);
+ }
+
+ public StatusImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setStatus(ServerStatus.OK);
+ }
+
+ public StatusImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setStatus(ServerStatus.OK);
+ }
+
+ public void setStatus(@ServerStatus.Enum int status) {
+ switch (status) {
+ case ServerStatus.CHECKING:
+ case ServerStatus.WAITING:
+ setImageResource(R.drawable.status_progress);
+ setBackgroundResource(R.drawable.yellow_circle);
+ break;
+ case ServerStatus.ERROR:
+ setImageResource(R.drawable.status_error);
+ setBackgroundResource(R.drawable.red_circle);
+ break;
+ case ServerStatus.OK:
+ setImageResource(R.drawable.status_ok);
+ setBackgroundResource(R.drawable.green_circle);
+ break;
+ }
+ }
+}
diff --git a/app/src/main/res/anim/fade_out.xml b/app/src/main/res/anim/fade_out.xml
new file mode 100644
index 0000000..900d3cc
--- /dev/null
+++ b/app/src/main/res/anim/fade_out.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/color/unchecked_chip_text.xml b/app/src/main/res/color/unchecked_chip_text.xml
deleted file mode 100644
index 8e7f4df..0000000
--- a/app/src/main/res/color/unchecked_chip_text.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable-hdpi-v11/ic_notification.png b/app/src/main/res/drawable-hdpi-v11/ic_notification.png
new file mode 100644
index 0000000..5c51b0b
Binary files /dev/null and b/app/src/main/res/drawable-hdpi-v11/ic_notification.png differ
diff --git a/app/src/main/res/drawable-mdpi-v11/ic_notification.png b/app/src/main/res/drawable-mdpi-v11/ic_notification.png
new file mode 100644
index 0000000..610685d
Binary files /dev/null and b/app/src/main/res/drawable-mdpi-v11/ic_notification.png differ
diff --git a/app/src/main/res/drawable-xhdpi-v11/ic_notification.png b/app/src/main/res/drawable-xhdpi-v11/ic_notification.png
new file mode 100644
index 0000000..88e634f
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi-v11/ic_notification.png differ
diff --git a/app/src/main/res/drawable-xxhdpi-v11/ic_notification.png b/app/src/main/res/drawable-xxhdpi-v11/ic_notification.png
new file mode 100644
index 0000000..dc4957d
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi-v11/ic_notification.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi-v11/ic_notification.png b/app/src/main/res/drawable-xxxhdpi-v11/ic_notification.png
new file mode 100644
index 0000000..af4779b
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi-v11/ic_notification.png differ
diff --git a/app/src/main/res/drawable/checked_chip.xml b/app/src/main/res/drawable/checked_chip.xml
deleted file mode 100644
index 85010f5..0000000
--- a/app/src/main/res/drawable/checked_chip.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/checked_chip_pressed.xml b/app/src/main/res/drawable/checked_chip_pressed.xml
deleted file mode 100644
index 0d7c176..0000000
--- a/app/src/main/res/drawable/checked_chip_pressed.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/checked_chip_selector.xml b/app/src/main/res/drawable/checked_chip_selector.xml
deleted file mode 100644
index fa9df00..0000000
--- a/app/src/main/res/drawable/checked_chip_selector.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/divider.xml b/app/src/main/res/drawable/divider.xml
index 7561a3c..98beb0c 100644
--- a/app/src/main/res/drawable/divider.xml
+++ b/app/src/main/res/drawable/divider.xml
@@ -1,4 +1,4 @@
-
-
-
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/green_circle.xml b/app/src/main/res/drawable/green_circle.xml
new file mode 100644
index 0000000..31bc563
--- /dev/null
+++ b/app/src/main/res/drawable/green_circle.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_action_close.xml b/app/src/main/res/drawable/ic_action_close.xml
index 241f9be..f4977c9 100644
--- a/app/src/main/res/drawable/ic_action_close.xml
+++ b/app/src/main/res/drawable/ic_action_close.xml
@@ -3,7 +3,7 @@
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
-
+
diff --git a/app/src/main/res/drawable/ic_action_delete.xml b/app/src/main/res/drawable/ic_action_delete.xml
index 901c3e1..d85947c 100644
--- a/app/src/main/res/drawable/ic_action_delete.xml
+++ b/app/src/main/res/drawable/ic_action_delete.xml
@@ -3,7 +3,7 @@
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_action_refresh.xml b/app/src/main/res/drawable/ic_action_refresh.xml
index 5175bda..85eb103 100644
--- a/app/src/main/res/drawable/ic_action_refresh.xml
+++ b/app/src/main/res/drawable/ic_action_refresh.xml
@@ -3,7 +3,7 @@
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml
index c0ca276..7b18128 100644
--- a/app/src/main/res/drawable/ic_add.xml
+++ b/app/src/main/res/drawable/ic_add.xml
@@ -3,7 +3,7 @@
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
-
+
diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml
deleted file mode 100644
index 00fc15d..0000000
--- a/app/src/main/res/drawable/ic_check.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_down_arrow.xml b/app/src/main/res/drawable/ic_down_arrow.xml
new file mode 100644
index 0000000..2341755
--- /dev/null
+++ b/app/src/main/res/drawable/ic_down_arrow.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/red_circle.xml b/app/src/main/res/drawable/red_circle.xml
new file mode 100644
index 0000000..d6bdd26
--- /dev/null
+++ b/app/src/main/res/drawable/red_circle.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/status_error.xml b/app/src/main/res/drawable/status_error.xml
new file mode 100644
index 0000000..6f67037
--- /dev/null
+++ b/app/src/main/res/drawable/status_error.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/status_ok.xml b/app/src/main/res/drawable/status_ok.xml
new file mode 100644
index 0000000..3ec2bc9
--- /dev/null
+++ b/app/src/main/res/drawable/status_ok.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/status_progress.xml b/app/src/main/res/drawable/status_progress.xml
new file mode 100644
index 0000000..c100e60
--- /dev/null
+++ b/app/src/main/res/drawable/status_progress.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/unchecked_chip.xml b/app/src/main/res/drawable/unchecked_chip.xml
deleted file mode 100644
index 1864bc5..0000000
--- a/app/src/main/res/drawable/unchecked_chip.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/unchecked_chip_pressed.xml b/app/src/main/res/drawable/unchecked_chip_pressed.xml
deleted file mode 100644
index c387d70..0000000
--- a/app/src/main/res/drawable/unchecked_chip_pressed.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/unchecked_chip_selector.xml b/app/src/main/res/drawable/unchecked_chip_selector.xml
deleted file mode 100644
index ba01f74..0000000
--- a/app/src/main/res/drawable/unchecked_chip_selector.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/yellow_circle.xml b/app/src/main/res/drawable/yellow_circle.xml
new file mode 100644
index 0000000..1201e65
--- /dev/null
+++ b/app/src/main/res/drawable/yellow_circle.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_addsite.xml b/app/src/main/res/layout/activity_addsite.xml
index 80f83da..4bf5e29 100644
--- a/app/src/main/res/layout/activity_addsite.xml
+++ b/app/src/main/res/layout/activity_addsite.xml
@@ -1,222 +1,242 @@
-
+ android:background="?colorPrimary"
+ android:orientation="vertical">
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="match_parent">
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/content_inset"
+ android:paddingLeft="@dimen/content_inset"
+ android:paddingRight="@dimen/content_inset">
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 1e95b7f..572f439 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,71 +1,105 @@
-
+ tools:context=".ui.MainActivity">
-
-
-
-
-
+ 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" />
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_viewsite.xml b/app/src/main/res/layout/activity_viewsite.xml
index 581c555..09fb8b4 100644
--- a/app/src/main/res/layout/activity_viewsite.xml
+++ b/app/src/main/res/layout/activity_viewsite.xml
@@ -1,309 +1,295 @@
-
+ android:background="?colorPrimary"
+ android:orientation="vertical">
-
-
-
+
-
-
-
-
+ android:layout_height="match_parent">
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/content_inset"
+ android:paddingLeft="@dimen/content_inset"
+ android:paddingRight="@dimen/content_inset"
+ android:paddingTop="@dimen/content_inset_half">
-
-
-
-
-
+ android:orientation="horizontal">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:layout_marginTop="@dimen/content_inset"
+ android:fontFamily="sans-serif"
+ android:text="@string/check_interval"
+ android:textColor="?colorAccent"
+ android:textSize="@dimen/caption_font_size" />
-
+ android:orientation="horizontal"
+ android:weightSum="2">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/include_app_bar.xml b/app/src/main/res/layout/include_app_bar.xml
deleted file mode 100644
index 6e60df9..0000000
--- a/app/src/main/res/layout/include_app_bar.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/include_divider.xml b/app/src/main/res/layout/include_divider.xml
deleted file mode 100644
index a0dc832..0000000
--- a/app/src/main/res/layout/include_divider.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
diff --git a/app/src/main/res/layout/include_empty_view.xml b/app/src/main/res/layout/include_empty_view.xml
deleted file mode 100644
index 1adf674..0000000
--- a/app/src/main/res/layout/include_empty_view.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
diff --git a/app/src/main/res/layout/list_item_server.xml b/app/src/main/res/layout/list_item_server.xml
index 04e37c5..3e9ae2c 100644
--- a/app/src/main/res/layout/list_item_server.xml
+++ b/app/src/main/res/layout/list_item_server.xml
@@ -1,86 +1,87 @@
-
+ android:paddingTop="@dimen/content_inset_less">
-
+
-
-
-
+ android:orientation="vertical">
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_spinner.xml b/app/src/main/res/layout/list_item_spinner.xml
new file mode 100644
index 0000000..32960dd
--- /dev/null
+++ b/app/src/main/res/layout/list_item_spinner.xml
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/viewcomponents/src/main/res/layout/list_item_spinner_dropdown.xml b/app/src/main/res/layout/list_item_spinner_dropdown.xml
similarity index 56%
rename from viewcomponents/src/main/res/layout/list_item_spinner_dropdown.xml
rename to app/src/main/res/layout/list_item_spinner_dropdown.xml
index 570e4f7..6b45ece 100644
--- a/viewcomponents/src/main/res/layout/list_item_spinner_dropdown.xml
+++ b/app/src/main/res/layout/list_item_spinner_dropdown.xml
@@ -1,10 +1,10 @@
-
+ android:textColor="?android:textColorPrimary"
+ android:textSize="@dimen/body_font_size" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_tag.xml b/app/src/main/res/layout/list_item_tag.xml
deleted file mode 100644
index 0ae5932..0000000
--- a/app/src/main/res/layout/list_item_tag.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
diff --git a/app/src/main/res/layout/menu_item_refresh_icon.xml b/app/src/main/res/layout/menu_item_refresh_icon.xml
deleted file mode 100644
index 0079df9..0000000
--- a/app/src/main/res/layout/menu_item_refresh_icon.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
diff --git a/app/src/main/res/menu/menu_addsite.xml b/app/src/main/res/menu/menu_addsite.xml
deleted file mode 100644
index e346eb9..0000000
--- a/app/src/main/res/menu/menu_addsite.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
index 5f2f23f..5df480e 100644
--- a/app/src/main/res/menu/menu_main.xml
+++ b/app/src/main/res/menu/menu_main.xml
@@ -1,10 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_viewsite.xml b/app/src/main/res/menu/menu_viewsite.xml
index fee6f5a..0d2128f 100644
--- a/app/src/main/res/menu/menu_viewsite.xml
+++ b/app/src/main/res/menu/menu_viewsite.xml
@@ -1,23 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index 80b730f..0000000
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index 80b730f..0000000
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
index a86dbab..d1698c3 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
deleted file mode 100644
index eb43a7b..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
deleted file mode 100644
index 4567198..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 60056be..cccfa5b 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
deleted file mode 100644
index 666c904..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
deleted file mode 100644
index 3baff41..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 27f30d2..a4f8356 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
deleted file mode 100644
index 4224797..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
deleted file mode 100644
index 1d29a54..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 60a8d1a..49dd226 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
deleted file mode 100644
index 7cf19eb..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
deleted file mode 100644
index 1548eb9..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index ac61bd3..bd57c86 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
deleted file mode 100644
index 9c43fc9..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
deleted file mode 100644
index 04806fd..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/values-v23/styles.xml b/app/src/main/res/values-v23/styles.xml
deleted file mode 100644
index c1a8339..0000000
--- a/app/src/main/res/values-v23/styles.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml
deleted file mode 100644
index c4efc7a..0000000
--- a/app/src/main/res/values-v27/styles.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
deleted file mode 100644
index 84ad226..0000000
--- a/app/src/main/res/values/arrays.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
- - @string/refresh_status
- - @string/duplicate_and_modify
- - @string/remove_site
-
-
-
- - Status Code
- - Search Term
- - JavaScript Evaluation
-
-
-
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
deleted file mode 100644
index 35d3041..0000000
--- a/app/src/main/res/values/attrs.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index f840cee..8127492 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,17 +1,16 @@
- #FFFFFF
- #F5F5F5
+ #455A64
+ #37474F
+ #FF6E40
- #212121
- #252525
+ #EEEEEE
- #303030
- #EEEEEE
+ #E53935
+ #FDD835
+ #43A047
- #FF6E40
- #E44615
- #40FF6E40
+ #37474F
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 510a2e9..629f464 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -1,7 +1,23 @@
- 28sp
- 6dp
- 4dp
+ 24sp
+ 20sp
+ 16sp
+ 14sp
+ 12sp
-
+ 8dp
+ 12dp
+ 16dp
+ 24dp
+ 32dp
+
+ 42dp
+ 4dp
+ 4dp
+ 8dp
+ 52dp
+ 300dp
+ 14sp
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
deleted file mode 100644
index 70daa76..0000000
--- a/app/src/main/res/values/ic_launcher_background.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- #758F9A
-
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3ac40a0..9e21c36 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,90 +1,81 @@
-
+
- Nock Nock
- Nock Nock %1$s
+ Nock Nock (BETA)
- No sites added!
+ Everything checks out!
+ Something\'s wrong! Tap for details.
+ Checking status…
+ Waiting…
- About
- Aidan Follestad.
- Website
+ No sites added!
+
+ About
+ Nock Nock, a simple app designed by Aidan Follestad.
+ Website
Twitter
+ Google+
GitHub
LinkedIn
Nock Nock is open source! Check out the GitHub page!
Icon by Kevin Aguilar of 221 Pixels.
-
View the Privacy Policy.
- ]]>
- Dark Mode
+ ]]>
+ Dismiss
+ Add Site
+ Site Name
+ Site URL
+ Check Interval
+ Done
+ Please enter a name!
+ Please enter a URL.
+ Please enter a valid URL.
- Dismiss
- Add Site
- Site Name
- Site display name
- Site URL
- https://yoursite.com
- Site Tags
- e.g. One,Two,Three
- Tags (e.g. One,Two,Three)
- Please enter a name!
- Please enter a URL.
- Please enter a valid URL.
- Please input a search term.
- Please enter a network timeout greater than 0.
- Certificate should be a valid file or content URI.
+ Request timed out! Your server is probably down.
+ Options
+ Already checking sites!
+ Remove Site
+ %1$s from your sites?]]>
+ Remove
+ Save
+ View Site
+ Last Check Result
+ Next Check
+ None (turned off)
+ None
- Options
- Remove Site
- Duplicate and Modify
- %1$s from your sites?]]>
- Remove
- Save Changes
- View Site
- Last Validation Result
- Next Validation
- Next Validation: %1$s
- Now
- None
+ Refresh Status
+ Drag the list down to manually refresh site statuses! Otherwise, they will be updated automatically in the background on chosen intervals.
+ Understood!
- Disable Automatic Validation
- %1$s? The site will not be checked in the background
- until you re-enable validation by tapping the checkmark (Save) icon. You can still manually
- perform validation by tapping the Refresh icon at the top of this page.
- ]]>
- Disable
- Enable Auto Validation & Save Changes
+
+ Warning: this app checks for server availability with HTTP requests. It\'s recommended that you use an HTTP URL.
+
+ var responseObj = JSON.parse(response);\nreturn responseObj.success === true;
+ function validate(response) {
+ }
+ Response Validation Mode
+ Search term…
- Network Response Timeout (ms)
- 10000
+ The HTTP status code is checked. If it\'s a successful status code, the site passes the check.
+ 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.
+ 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.
- SSL Certificate
- (Automatic)
- Browse
+
+ - Minute(s)
+ - Hour(s)
+ - Day(s)
+ - Weeks(s)
+
- Refresh Status
+
+ - @string/refresh_status
+ - @string/remove_site
+
-
- Warning: this app validates sites availability with HTTP requests. It\'s recommended that you
- use an HTTP URL.
-
- Response Validation Mode
- Search term…
-
-
- The HTTP status code is checked. If it\'s a successful status code, the site passes validation.
-
-
- 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 validation.
-
-
- 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 validation. Throw an
- exception to pass custom error messages to Nock Nock.
-
-
- Please install a web browser app, such as Google Chrome.
+
+ - Status Code
+ - Search Term
+ - JavaScript Evaluation
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index e4b435a..1a75349 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,13 +1,35 @@
-
+
+ - #212121
+ - #727272
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/styles_parents.xml b/app/src/main/res/values/styles_parents.xml
deleted file mode 100644
index dc5e3d6..0000000
--- a/app/src/main/res/values/styles_parents.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values/styles_text.xml b/app/src/main/res/values/styles_text.xml
deleted file mode 100644
index df25036..0000000
--- a/app/src/main/res/values/styles_text.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/test/java/com/afollestad/nocknock/TestData.kt b/app/src/test/java/com/afollestad/nocknock/TestData.kt
deleted file mode 100644
index e5e4c29..0000000
--- a/app/src/test/java/com/afollestad/nocknock/TestData.kt
+++ /dev/null
@@ -1,216 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock
-
-import android.app.PendingIntent
-import android.content.Intent
-import android.content.IntentFilter
-import com.afollestad.nocknock.data.AppDatabase
-import com.afollestad.nocknock.data.HeaderDao
-import com.afollestad.nocknock.data.RetryPolicyDao
-import com.afollestad.nocknock.data.SiteDao
-import com.afollestad.nocknock.data.SiteSettingsDao
-import com.afollestad.nocknock.data.ValidationResultsDao
-import com.afollestad.nocknock.data.model.Header
-import com.afollestad.nocknock.data.model.RetryPolicy
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.data.model.SiteSettings
-import com.afollestad.nocknock.data.model.Status
-import com.afollestad.nocknock.data.model.Status.OK
-import com.afollestad.nocknock.data.model.ValidationMode
-import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.data.model.ValidationResult
-import com.afollestad.nocknock.utilities.providers.CanNotifyModel
-import com.afollestad.nocknock.utilities.providers.IntentProvider
-import com.nhaarman.mockitokotlin2.any
-import com.nhaarman.mockitokotlin2.doAnswer
-import com.nhaarman.mockitokotlin2.doReturn
-import com.nhaarman.mockitokotlin2.isA
-import com.nhaarman.mockitokotlin2.mock
-import java.lang.System.currentTimeMillis
-
-fun fakeIntent(action: String): Intent {
- return mock {
- on { getAction() } doReturn action
- }
-}
-
-fun fakeSettingsModel(
- id: Long,
- validationMode: ValidationMode = STATUS_CODE
-) = SiteSettings(
- siteId = id,
- validationIntervalMs = 600000,
- validationMode = validationMode,
- validationArgs = null,
- disabled = false,
- networkTimeout = 10000,
- certificate = null
-)
-
-fun fakeResultModel(
- id: Long,
- status: Status = OK,
- reason: String? = null
-) = ValidationResult(
- siteId = id,
- status = status,
- reason = reason,
- timestampMs = currentTimeMillis()
-)
-
-fun fakeRetryPolicy(
- id: Long,
- count: Int = 3,
- minutes: Int = 6
-) = RetryPolicy(
- siteId = id,
- count = count,
- minutes = minutes
-)
-
-fun fakeHeaders(siteId: Long): List {
- return listOf(
- Header(id = siteId + 1, siteId = siteId, key = "Content-Type", value = "text/html"),
- Header(id = siteId + 2, siteId = siteId, key = "User-Agent", value = "NockNock")
- )
-}
-
-fun fakeModel(
- id: Long,
- tags: String = ""
-) = Site(
- id = id,
- name = "Test",
- url = "https://test.com",
- tags = tags,
- settings = fakeSettingsModel(id),
- lastResult = fakeResultModel(id),
- retryPolicy = fakeRetryPolicy(id),
- headers = fakeHeaders(id)
-)
-
-val MOCK_MODEL_1 = fakeModel(1, tags = "one,two")
-val MOCK_MODEL_2 = fakeModel(2, tags = "three,four")
-val MOCK_MODEL_3 = fakeModel(3, tags = "five,six")
-
-val ALL_MOCK_MODELS = listOf(MOCK_MODEL_1, MOCK_MODEL_2, MOCK_MODEL_3)
-
-fun mockDatabase(): AppDatabase {
- val siteDao = mock {
- on { insert(isA()) } doReturn 1
- on { one(isA()) } doAnswer { inv ->
- val id = inv.getArgument(0)
- return@doAnswer when (id) {
- 1L -> listOf(MOCK_MODEL_1)
- 2L -> listOf(MOCK_MODEL_2)
- 3L -> listOf(MOCK_MODEL_3)
- else -> listOf()
- }
- }
- on { all() } doReturn ALL_MOCK_MODELS
- on { update(isA()) } doAnswer { inv ->
- return@doAnswer inv.arguments.size
- }
- on { delete(isA()) } doAnswer { inv ->
- return@doAnswer inv.arguments.size
- }
- }
- val settingsDao = mock {
- on { insert(isA()) } doReturn 1L
- on { forSite(isA()) } doAnswer { inv ->
- val id = inv.getArgument(0)
- return@doAnswer when (id) {
- 1L -> listOf(MOCK_MODEL_1.settings!!)
- 2L -> listOf(MOCK_MODEL_2.settings!!)
- 3L -> listOf(MOCK_MODEL_3.settings!!)
- else -> listOf()
- }
- }
- on { update(isA()) } doReturn 1
- on { delete(isA()) } doReturn 1
- }
- val resultsDao = mock {
- on { insert(isA()) } doReturn 1L
- on { forSite(isA()) } doAnswer { inv ->
- val id = inv.getArgument(0)
- return@doAnswer when (id) {
- 1L -> listOf(MOCK_MODEL_1.lastResult!!)
- 2L -> listOf(MOCK_MODEL_2.lastResult!!)
- 3L -> listOf(MOCK_MODEL_3.lastResult!!)
- else -> listOf()
- }
- }
- on { update(isA()) } doReturn 1
- on { delete(isA()) } doReturn 1
- }
- val retryDao = mock {
- on { insert(isA()) } doReturn 1L
- on { forSite(isA()) } doAnswer { inv ->
- val id = inv.getArgument(0)
- return@doAnswer when (id) {
- 1L -> listOf(MOCK_MODEL_1.retryPolicy!!)
- 2L -> listOf(MOCK_MODEL_2.retryPolicy!!)
- 3L -> listOf(MOCK_MODEL_3.retryPolicy!!)
- else -> listOf()
- }
- }
- on { update(isA()) } doReturn 1
- on { delete(isA()) } doReturn 1
- }
- val headerDao = mock {
- on { all() } doReturn MOCK_MODEL_1.headers + MOCK_MODEL_2.headers + MOCK_MODEL_3.headers
- on { forSite(isA()) } doAnswer { inv ->
- val id = inv.getArgument(0)
- return@doAnswer when (id) {
- 1L -> MOCK_MODEL_1.headers
- 2L -> MOCK_MODEL_2.headers
- 3L -> MOCK_MODEL_3.headers
- else -> listOf()
- }
- }
- on { insert(isA()) } doReturn 1L
- on { insert(isA>()) } doReturn listOf(1L, 2L)
- on { update(isA()) } doReturn 1
- on { delete(isA()) } doReturn 1
- }
-
- return mock {
- on { siteDao() } doReturn siteDao
- on { siteSettingsDao() } doReturn settingsDao
- on { validationResultsDao() } doReturn resultsDao
- on { retryPolicyDao() } doReturn retryDao
- on { headerDao() } doReturn headerDao
- }
-}
-
-fun mockIntentProvider() = object : IntentProvider {
- override fun createFilter(vararg actions: String): IntentFilter {
- return mock {
- on { this.getAction(any()) } doAnswer { inv ->
- val index = inv.getArgument(0)
- return@doAnswer actions[index]
- }
- on { this.actionsIterator() } doReturn actions.iterator()
- on { this.countActions() } doReturn actions.size
- }
- }
-
- override fun getPendingIntentForViewSite(model: CanNotifyModel): PendingIntent {
- // basically no-op right now
- return mock()
- }
-}
diff --git a/app/src/test/java/com/afollestad/nocknock/broadcasts/StatusUpdateIntentReceiverTest.kt b/app/src/test/java/com/afollestad/nocknock/broadcasts/StatusUpdateIntentReceiverTest.kt
deleted file mode 100644
index e3e51b3..0000000
--- a/app/src/test/java/com/afollestad/nocknock/broadcasts/StatusUpdateIntentReceiverTest.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.broadcasts
-
-import android.app.Application
-import android.content.IntentFilter
-import com.afollestad.nocknock.MOCK_MODEL_2
-import com.afollestad.nocknock.engine.validation.ValidationJob.Companion.ACTION_STATUS_UPDATE
-import com.afollestad.nocknock.engine.validation.ValidationJob.Companion.KEY_UPDATE_MODEL
-import com.afollestad.nocknock.fakeIntent
-import com.afollestad.nocknock.mockIntentProvider
-import com.google.common.truth.Truth.assertThat
-import com.nhaarman.mockitokotlin2.argumentCaptor
-import com.nhaarman.mockitokotlin2.doReturn
-import com.nhaarman.mockitokotlin2.eq
-import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.times
-import com.nhaarman.mockitokotlin2.verify
-import com.nhaarman.mockitokotlin2.whenever
-import org.junit.Test
-
-/** @author Aidan Follestad (@afollestad) */
-class StatusUpdateIntentReceiverTest {
-
- private val app = mock()
- private val intentProvider = mockIntentProvider()
- private val callback = mock()
-
- private val receiver = StatusUpdateIntentReceiver(app, intentProvider, callback)
-
- @Test fun onReceive() {
- val badIntent = fakeIntent("Hello World")
- receiver.intentReceiver.onReceive(app, badIntent)
-
- val goodIntent = fakeIntent(ACTION_STATUS_UPDATE)
- whenever(goodIntent.getSerializableExtra(KEY_UPDATE_MODEL))
- .doReturn(MOCK_MODEL_2)
-
- receiver.intentReceiver.onReceive(app, goodIntent)
- verify(callback, times(1)).invoke(MOCK_MODEL_2)
- }
-
- @Test fun onResume() {
- receiver.onResume()
-
- val filterCaptor = argumentCaptor()
- verify(app).registerReceiver(eq(receiver.intentReceiver), filterCaptor.capture())
-
- val actionIterator = filterCaptor.firstValue.actionsIterator()
- assertThat(actionIterator.hasNext()).isTrue()
- val filterAction = actionIterator.next()
- assertThat(filterAction).isEqualTo(ACTION_STATUS_UPDATE)
- assertThat(actionIterator.hasNext()).isFalse()
- }
-
- @Test fun onPause() {
- receiver.onPause()
- verify(app).unregisterReceiver(receiver.intentReceiver)
- }
-}
diff --git a/app/src/test/java/com/afollestad/nocknock/ui/addsite/AddSiteViewModelTest.kt b/app/src/test/java/com/afollestad/nocknock/ui/addsite/AddSiteViewModelTest.kt
deleted file mode 100644
index 1319310..0000000
--- a/app/src/test/java/com/afollestad/nocknock/ui/addsite/AddSiteViewModelTest.kt
+++ /dev/null
@@ -1,206 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.ui.addsite
-
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import com.afollestad.nocknock.R
-import com.afollestad.nocknock.data.model.Header
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.data.model.SiteSettings
-import com.afollestad.nocknock.data.model.Status.WAITING
-import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
-import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
-import com.afollestad.nocknock.data.model.ValidationResult
-import com.afollestad.nocknock.engine.validation.ValidationExecutor
-import com.afollestad.nocknock.mockDatabase
-import com.afollestad.nocknock.utilities.ext.MINUTE
-import com.afollestad.nocknock.utilities.livedata.test
-import com.google.common.truth.Truth.assertThat
-import com.nhaarman.mockitokotlin2.argumentCaptor
-import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.verify
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
-import org.junit.After
-import org.junit.Rule
-import org.junit.Test
-
-/** @author Aidan Follestad (@afollestad) */
-@ExperimentalCoroutinesApi
-class AddSiteViewModelTest {
-
- private val database = mockDatabase()
- private val validationManager = mock()
-
- @Rule @JvmField val rule = InstantTaskExecutorRule()
-
- private val viewModel = AddSiteViewModel(
- database,
- validationManager,
- Dispatchers.Unconfined,
- Dispatchers.Unconfined
- )
-
- @After fun tearDown() = viewModel.destroy()
-
- @Test fun setDefaults() {
- viewModel.setDefaults()
-
- assertThat(viewModel.name.value).isNull()
- assertThat(viewModel.url.value).isNull()
- assertThat(viewModel.timeout.value).isEqualTo(10000)
- assertThat(viewModel.validationMode.value).isEqualTo(STATUS_CODE)
- assertThat(viewModel.validationSearchTerm.value).isNull()
- assertThat(viewModel.validationScript.value).isNull()
- assertThat(viewModel.checkIntervalValue.value).isEqualTo(0)
- assertThat(viewModel.checkIntervalUnit.value).isEqualTo(MINUTE)
- }
-
- @Test fun onUrlWarningVisibility() {
- val urlWarningVisibility = viewModel.onUrlWarningVisibility()
- .test()
-
- viewModel.url.value = ""
- urlWarningVisibility.assertValues(false)
-
- viewModel.url.value = "helloworld"
- urlWarningVisibility.assertValues(true)
-
- viewModel.url.value = "http://helloworld.com"
- urlWarningVisibility.assertValues(false)
-
- viewModel.url.value = "ftp://helloworld.com"
- urlWarningVisibility.assertValues(true)
- }
-
- @Test fun onValidationModeDescription() {
- val description = viewModel.onValidationModeDescription()
- .test()
-
- viewModel.validationMode.value = STATUS_CODE
- description.assertValues(R.string.validation_mode_status_desc)
-
- viewModel.validationMode.value = TERM_SEARCH
- description.assertValues(R.string.validation_mode_term_desc)
-
- viewModel.validationMode.value = JAVASCRIPT
- description.assertValues(R.string.validation_mode_javascript_desc)
- }
-
- @Test fun onValidationSearchTermVisibility() {
- val visibility = viewModel.onValidationSearchTermVisibility()
- .test()
-
- viewModel.validationMode.value = STATUS_CODE
- visibility.assertValues(false)
-
- viewModel.validationMode.value = TERM_SEARCH
- visibility.assertValues(true)
-
- viewModel.validationMode.value = JAVASCRIPT
- visibility.assertValues(false)
- }
-
- @Test fun onValidationScriptVisibility() {
- val visibility = viewModel.onValidationScriptVisibility()
- .test()
-
- viewModel.validationMode.value = STATUS_CODE
- visibility.assertValues(false)
-
- viewModel.validationMode.value = TERM_SEARCH
- visibility.assertValues(false)
-
- viewModel.validationMode.value = JAVASCRIPT
- visibility.assertValues(true)
- }
-
- @Test fun getCheckIntervalMs() {
- viewModel.checkIntervalValue.value = 3
- viewModel.checkIntervalUnit.value = 200
- assertThat(viewModel.getCheckIntervalMs()).isEqualTo(600L)
- }
-
- @Test fun getValidationArgs() {
- viewModel.validationSearchTerm.value = "One"
- viewModel.validationScript.value = "Two"
-
- viewModel.validationMode.value = STATUS_CODE
- assertThat(viewModel.getValidationArgs()).isNull()
-
- viewModel.validationMode.value = TERM_SEARCH
- assertThat(viewModel.getValidationArgs()).isEqualTo("One")
-
- viewModel.validationMode.value = JAVASCRIPT
- assertThat(viewModel.getValidationArgs()).isEqualTo("Two")
- }
-
- @Test fun commit_success() = runBlocking {
- val isLoading = viewModel.onIsLoading()
- .test()
-
- fillInModel()
- val onDone = mock<() -> Unit>()
- viewModel.commit(onDone)
-
- val siteCaptor = argumentCaptor()
- val settingsCaptor = argumentCaptor()
- val validationResultCaptor = argumentCaptor()
-
- isLoading.assertValues(true, false)
- verify(database.siteDao()).insert(siteCaptor.capture())
- verify(database.siteSettingsDao()).insert(settingsCaptor.capture())
- verify(database.validationResultsDao()).insert(validationResultCaptor.capture())
-
- val settings = settingsCaptor.firstValue
- val result = validationResultCaptor.firstValue.copy(siteId = 1)
- val model = siteCaptor.firstValue.copy(
- id = 1, // fill it in because our insert captor doesn't catch this
- settings = settings,
- lastResult = result
- )
-
- assertThat(result.reason).isNull()
- assertThat(result.status).isEqualTo(WAITING)
-
- verify(validationManager).scheduleValidation(
- site = model,
- rightNow = true,
- cancelPrevious = true,
- fromFinishingJob = false
- )
-
- verify(onDone).invoke()
- }
-
- private fun fillInModel() = viewModel.apply {
- name.value = "Welcome to Wakanda"
- url.value = "https://www.wakanda.gov"
- timeout.value = 10000
- validationMode.value = TERM_SEARCH
- validationSearchTerm.value = "T'Challa"
- validationScript.value = null
- checkIntervalValue.value = 60
- checkIntervalUnit.value = 1000
- tags.value = "one,two"
- headers.value = listOf(
- Header(2L, 1L, key = "Content-Type", value = "text/html"),
- Header(3L, 1L, key = "User-Agent", value = "NockNock")
- )
- }
-}
diff --git a/app/src/test/java/com/afollestad/nocknock/ui/main/MainViewModelTest.kt b/app/src/test/java/com/afollestad/nocknock/ui/main/MainViewModelTest.kt
deleted file mode 100644
index 593996e..0000000
--- a/app/src/test/java/com/afollestad/nocknock/ui/main/MainViewModelTest.kt
+++ /dev/null
@@ -1,189 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.ui.main
-
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import com.afollestad.nocknock.ALL_MOCK_MODELS
-import com.afollestad.nocknock.MOCK_MODEL_1
-import com.afollestad.nocknock.MOCK_MODEL_2
-import com.afollestad.nocknock.MOCK_MODEL_3
-import com.afollestad.nocknock.engine.validation.ValidationExecutor
-import com.afollestad.nocknock.mockDatabase
-import com.afollestad.nocknock.notifications.NockNotificationManager
-import com.afollestad.nocknock.utilities.livedata.test
-import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.verify
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
-import org.junit.After
-import org.junit.Rule
-import org.junit.Test
-
-/** @author Aidan Follestad (@afollestad) */
-@ExperimentalCoroutinesApi
-class MainViewModelTest {
-
- private val database = mockDatabase()
- private val notificationManager = mock()
- private val validationManager = mock()
-
- @Rule @JvmField val rule = InstantTaskExecutorRule()
-
- private val viewModel = MainViewModel(
- database,
- notificationManager,
- validationManager,
- Dispatchers.Unconfined,
- Dispatchers.Unconfined
- )
-
- @After fun tearDown() = viewModel.destroy()
-
- @Test fun onResume() = runBlocking {
- val isLoading = viewModel.onIsLoading()
- .test()
- val emptyTextVisibility = viewModel.onEmptyTextVisibility()
- .test()
- val sites = viewModel.onSites()
- .test()
- val tags = viewModel.onTags()
- .test()
- val tagsVisibility = viewModel.onTagsListVisibility()
- .test()
-
- viewModel.onResume()
-
- verify(notificationManager).cancelStatusNotifications()
- verify(validationManager).ensureScheduledValidations()
-
- sites.assertValues(ALL_MOCK_MODELS)
- isLoading.assertValues(true, false)
- emptyTextVisibility.assertValues(false, false)
- tags.assertValues(listOf("one", "two", "three", "four", "five", "six").sorted())
- tagsVisibility.assertValues(true)
- }
-
- @Test fun onTagSelection() = runBlocking {
- val isLoading = viewModel.onIsLoading()
- .test()
- val emptyTextVisibility = viewModel.onEmptyTextVisibility()
- .test()
- val sites = viewModel.onSites()
- .test()
- val tags = viewModel.onTags()
- .test()
- val tagsVisibility = viewModel.onTagsListVisibility()
- .test()
-
- viewModel.onTagSelection(listOf("four", "six"))
-
- verify(notificationManager).cancelStatusNotifications()
- verify(validationManager).ensureScheduledValidations()
-
- sites.assertValues(listOf(MOCK_MODEL_2, MOCK_MODEL_3))
- isLoading.assertValues(true, false)
- emptyTextVisibility.assertValues(false, false)
- tags.assertValues(listOf("one", "two", "three", "four", "five", "six").sorted())
- tagsVisibility.assertValues(true)
- }
-
- @Test fun postSiteUpdate_notFound() {
- val sites = viewModel.onSites()
- .test()
- viewModel.postSiteUpdate(MOCK_MODEL_1)
- sites.assertNoValues()
- }
-
- @Test fun postSiteUpdate() {
- val sites = viewModel.onSites()
- .test()
-
- viewModel.onResume()
- sites.assertValues(ALL_MOCK_MODELS)
-
- val updatedModel2 = MOCK_MODEL_2.copy(
- name = "Wakanda Forever!!!"
- )
- val updatedSites = ALL_MOCK_MODELS.toMutableList()
- .apply {
- this[1] = updatedModel2
- }
- viewModel.postSiteUpdate(updatedModel2)
-
- sites.assertValues(updatedSites)
- }
-
- @Test fun refreshSite() {
- viewModel.refreshSite(MOCK_MODEL_3)
-
- verify(validationManager).scheduleValidation(
- site = MOCK_MODEL_3,
- rightNow = true,
- cancelPrevious = true
- )
- }
-
- @Test fun removeSite_notFound() {
- val sites = viewModel.onSites()
- .test()
- val isLoading = viewModel.onIsLoading()
- .test()
-
- viewModel.onResume()
- sites.assertValues(ALL_MOCK_MODELS)
- isLoading.assertValues(true, false)
-
- val modifiedModel = MOCK_MODEL_1.copy(id = 11111)
- viewModel.removeSite(modifiedModel)
-
- sites.assertNoValues()
- isLoading.assertValues(true, false)
-
- verify(validationManager).cancelScheduledValidation(modifiedModel)
- verify(notificationManager).cancelStatusNotification(modifiedModel)
- verify(database.siteDao()).delete(modifiedModel)
- verify(database.siteSettingsDao()).delete(modifiedModel.settings!!)
- }
-
- @Test fun removeSite() {
- val sites = viewModel.onSites()
- .test()
- val emptyTextVisibility = viewModel.onEmptyTextVisibility()
- .test()
- val isLoading = viewModel.onIsLoading()
- .test()
-
- viewModel.onResume()
- sites.assertValues(ALL_MOCK_MODELS)
- isLoading.assertValues(true, false)
-
- val modelsWithout1 = ALL_MOCK_MODELS.toMutableList()
- .apply {
- removeAt(0)
- }
- viewModel.removeSite(MOCK_MODEL_1)
-
- sites.assertValues(modelsWithout1)
- isLoading.assertValues(true, false)
- emptyTextVisibility.assertValues(false, false, false)
-
- verify(validationManager).cancelScheduledValidation(MOCK_MODEL_1)
- verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
- verify(database.siteDao()).delete(MOCK_MODEL_1)
- verify(database.siteSettingsDao()).delete(MOCK_MODEL_1.settings!!)
- }
-}
diff --git a/app/src/test/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelTest.kt b/app/src/test/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelTest.kt
deleted file mode 100644
index 47fe6e0..0000000
--- a/app/src/test/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelTest.kt
+++ /dev/null
@@ -1,395 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.ui.viewsite
-
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import com.afollestad.nocknock.MOCK_MODEL_1
-import com.afollestad.nocknock.R
-import com.afollestad.nocknock.data.model.Header
-import com.afollestad.nocknock.data.model.RetryPolicy
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.data.model.SiteSettings
-import com.afollestad.nocknock.data.model.Status.CHECKING
-import com.afollestad.nocknock.data.model.Status.ERROR
-import com.afollestad.nocknock.data.model.Status.OK
-import com.afollestad.nocknock.data.model.Status.WAITING
-import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
-import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
-import com.afollestad.nocknock.data.model.ValidationResult
-import com.afollestad.nocknock.engine.validation.ValidationExecutor
-import com.afollestad.nocknock.fakeRetryPolicy
-import com.afollestad.nocknock.mockDatabase
-import com.afollestad.nocknock.notifications.NockNotificationManager
-import com.afollestad.nocknock.utilities.livedata.test
-import com.afollestad.nocknock.utilities.providers.StringProvider
-import com.google.common.truth.Truth
-import com.google.common.truth.Truth.assertThat
-import com.nhaarman.mockitokotlin2.any
-import com.nhaarman.mockitokotlin2.argumentCaptor
-import com.nhaarman.mockitokotlin2.doAnswer
-import com.nhaarman.mockitokotlin2.doReturn
-import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.verify
-import com.nhaarman.mockitokotlin2.whenever
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
-import org.junit.After
-import org.junit.Rule
-import org.junit.Test
-import java.util.Calendar
-
-/** @author Aidan Follestad (@afollestad) */
-@ExperimentalCoroutinesApi
-class ViewSiteViewModelTest {
-
- companion object {
- private const val TEXT_NONE = "None"
- private const val TEXT_EVERYTHING_CHECKS_OUT = "Everything checks out!"
- private const val TEXT_WAITING = "Waiting..."
- private const val TEXT_CHECKING = "Checking..."
- private const val TEXT_CHECKS_DISABLED = "Automatic Checks Disabled"
- }
-
- private val stringProvider = mock {
- on { get(any()) } doAnswer { inv ->
- val id = inv.getArgument(0)
- when (id) {
- R.string.none -> TEXT_NONE
- R.string.everything_checks_out -> TEXT_EVERYTHING_CHECKS_OUT
- R.string.waiting -> TEXT_WAITING
- R.string.checking_status -> TEXT_CHECKING
- R.string.auto_checks_disabled -> TEXT_CHECKS_DISABLED
- else -> ""
- }
- }
- }
- private val database = mockDatabase()
- private val validationManager = mock()
- private val notificationManager = mock()
-
- @Rule @JvmField val rule = InstantTaskExecutorRule()
-
- private val viewModel = ViewSiteViewModel(
- stringProvider,
- database,
- notificationManager,
- validationManager,
- Dispatchers.Unconfined,
- Dispatchers.Unconfined
- )
-
- @After fun tearDown() = viewModel.destroy()
-
- @Test fun onUrlWarningVisibility() {
- val urlWarningVisibility = viewModel.onUrlWarningVisibility()
- .test()
-
- viewModel.url.value = ""
- urlWarningVisibility.assertValues(false)
-
- viewModel.url.value = "helloworld"
- urlWarningVisibility.assertValues(true)
-
- viewModel.url.value = "http://helloworld.com"
- urlWarningVisibility.assertValues(false)
-
- viewModel.url.value = "ftp://helloworld.com"
- urlWarningVisibility.assertValues(true)
- }
-
- @Test fun onValidationModeDescription() {
- val description = viewModel.onValidationModeDescription()
- .test()
-
- viewModel.validationMode.value = STATUS_CODE
- description.assertValues(R.string.validation_mode_status_desc)
-
- viewModel.validationMode.value = TERM_SEARCH
- description.assertValues(R.string.validation_mode_term_desc)
-
- viewModel.validationMode.value = JAVASCRIPT
- description.assertValues(R.string.validation_mode_javascript_desc)
- }
-
- @Test fun onValidationSearchTermVisibility() {
- val visibility = viewModel.onValidationSearchTermVisibility()
- .test()
-
- viewModel.validationMode.value = STATUS_CODE
- visibility.assertValues(false)
-
- viewModel.validationMode.value = TERM_SEARCH
- visibility.assertValues(true)
-
- viewModel.validationMode.value = JAVASCRIPT
- visibility.assertValues(false)
- }
-
- @Test fun onValidationScriptVisibility() {
- val visibility = viewModel.onValidationScriptVisibility()
- .test()
-
- viewModel.validationMode.value = STATUS_CODE
- visibility.assertValues(false)
-
- viewModel.validationMode.value = TERM_SEARCH
- visibility.assertValues(false)
-
- viewModel.validationMode.value = JAVASCRIPT
- visibility.assertValues(true)
- }
-
- @Test fun onDisableChecksVisibility() {
- val visibility = viewModel.onDisableChecksVisibility()
- .test()
-
- viewModel.disabled.value = false
- visibility.assertValues(true)
-
- viewModel.disabled.value = true
- visibility.assertValues(false)
- }
-
- @Test fun onDoneButtonText() {
- val text = viewModel.onDoneButtonText()
- .test()
-
- viewModel.disabled.value = false
- text.assertValues(R.string.save_changes)
-
- viewModel.disabled.value = true
- text.assertValues(R.string.renable_and_save_changes)
- }
-
- @Test fun onLastCheckResultText() {
- val text = viewModel.onLastCheckResultText()
- .test()
- val lastResult = ValidationResult(
- siteId = 1,
- timestampMs = 10,
- status = OK,
- reason = "Hello, world!"
- )
-
- viewModel.lastResult.value = null
- text.assertValues(TEXT_NONE)
-
- viewModel.lastResult.value = lastResult
- text.assertValues(TEXT_EVERYTHING_CHECKS_OUT)
-
- viewModel.lastResult.value = lastResult.copy(status = WAITING)
- text.assertValues(TEXT_WAITING)
-
- viewModel.lastResult.value = lastResult.copy(status = CHECKING)
- text.assertValues(TEXT_CHECKING)
-
- viewModel.lastResult.value = lastResult.copy(
- status = ERROR,
- reason = "Uh oh!"
- )
- text.assertValues("Uh oh!")
- }
-
- @Test fun onNextCheckText() {
- viewModel.checkIntervalValue.value = 60
- viewModel.checkIntervalUnit.value = 5000
-
- val text = viewModel.onNextCheckText()
- .test()
- val calendar = Calendar.getInstance()
- .apply {
- set(Calendar.YEAR, 2018)
- set(Calendar.MONTH, Calendar.DECEMBER)
- set(Calendar.DAY_OF_MONTH, 6)
- set(Calendar.HOUR_OF_DAY, 8)
- set(Calendar.MINUTE, 30)
- set(Calendar.SECOND, 0)
- }
- val lastResult = ValidationResult(
- siteId = 1,
- timestampMs = calendar.timeInMillis,
- status = OK,
- reason = null
- )
-
- viewModel.disabled.value = true
- viewModel.lastResult.value = lastResult
- text.assertValues(TEXT_CHECKS_DISABLED)
-
- viewModel.disabled.value = false
- text.assertValues("December 6, 8:35 AM")
- }
-
- @Test fun getCheckIntervalMs() {
- viewModel.checkIntervalValue.value = 3
- viewModel.checkIntervalUnit.value = 200
- Truth.assertThat(viewModel.getCheckIntervalMs())
- .isEqualTo(600L)
- }
-
- @Test fun getValidationArgs() {
- viewModel.validationSearchTerm.value = "One"
- viewModel.validationScript.value = "Two"
-
- viewModel.validationMode.value = STATUS_CODE
- Truth.assertThat(viewModel.getValidationArgs())
- .isNull()
-
- viewModel.validationMode.value = TERM_SEARCH
- Truth.assertThat(viewModel.getValidationArgs())
- .isEqualTo("One")
-
- viewModel.validationMode.value = JAVASCRIPT
- Truth.assertThat(viewModel.getValidationArgs())
- .isEqualTo("Two")
- }
-
- @Test fun commit_success() = runBlocking {
- whenever(database.retryPolicyDao().forSite(any())).doReturn(listOf(fakeRetryPolicy(1)))
-
- val isLoading = viewModel.onIsLoading()
- .test()
-
- fillInModel()
- val onDone = mock<() -> Unit>()
-
- viewModel.site = MOCK_MODEL_1
- viewModel.commit(onDone)
-
- val siteCaptor = argumentCaptor()
- val settingsCaptor = argumentCaptor()
- val resultCaptor = argumentCaptor()
- val retryPolicyCaptor = argumentCaptor()
-
- isLoading.assertValues(true, false)
- verify(database.siteDao()).update(siteCaptor.capture())
- verify(database.siteSettingsDao()).update(settingsCaptor.capture())
- verify(database.validationResultsDao()).update(resultCaptor.capture())
- verify(database.retryPolicyDao()).update(retryPolicyCaptor.capture())
-
- // From fillInModel() below
- val updatedSettings = MOCK_MODEL_1.settings!!.copy(
- networkTimeout = 30000,
- validationMode = JAVASCRIPT,
- validationArgs = "throw 'Oh no!'",
- disabled = false,
- validationIntervalMs = 24 * 60000
- )
- val updatedResult = MOCK_MODEL_1.lastResult!!.copy(
- status = WAITING
- )
- val retryPolicy = retryPolicyCaptor.firstValue
- val updatedModel = MOCK_MODEL_1.copy(
- name = "Hello There",
- url = "https://www.hellothere.com",
- settings = updatedSettings,
- lastResult = updatedResult,
- retryPolicy = retryPolicy
- )
-
- assertThat(siteCaptor.firstValue).isEqualTo(updatedModel)
- assertThat(settingsCaptor.firstValue).isEqualTo(updatedSettings)
- assertThat(resultCaptor.firstValue).isEqualTo(updatedResult)
-
- verify(validationManager).scheduleValidation(
- site = updatedModel,
- rightNow = true,
- cancelPrevious = true,
- fromFinishingJob = false
- )
-
- verify(onDone).invoke()
- }
-
- @Test fun checkNow() {
- val status = viewModel.status.test()
-
- viewModel.site = MOCK_MODEL_1
- val expectedModel = MOCK_MODEL_1.copy(
- lastResult = MOCK_MODEL_1.lastResult!!.copy(
- status = WAITING
- )
- )
-
- viewModel.checkNow()
- verify(validationManager).scheduleValidation(
- site = expectedModel,
- rightNow = true,
- cancelPrevious = true
- )
- status.assertValues(WAITING)
- }
-
- @Test fun removeSite() {
- val isLoading = viewModel.onIsLoading()
- .test()
- val onDone = mock<() -> Unit>()
-
- viewModel.site = MOCK_MODEL_1
- viewModel.removeSite(onDone)
- isLoading.assertValues(true, false)
-
- verify(validationManager).cancelScheduledValidation(MOCK_MODEL_1)
- verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
- verify(database.siteDao()).delete(MOCK_MODEL_1)
- verify(database.siteSettingsDao()).delete(MOCK_MODEL_1.settings!!)
- verify(database.validationResultsDao()).delete(MOCK_MODEL_1.lastResult!!)
- verify(onDone).invoke()
- }
-
- @Test fun disableSite() {
- val isLoading = viewModel.onIsLoading()
- .test()
- val disabled = viewModel.disabled.test()
-
- viewModel.site = MOCK_MODEL_1
- viewModel.disableSite()
- isLoading.assertValues(true, false)
- disabled.assertValues(true)
-
- val expectedSite = MOCK_MODEL_1.copy(
- settings = MOCK_MODEL_1.settings!!.copy(
- disabled = true
- )
- )
-
- verify(validationManager).cancelScheduledValidation(MOCK_MODEL_1)
- verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
- verify(database.siteDao()).update(expectedSite)
- verify(database.siteSettingsDao()).update(expectedSite.settings!!)
- verify(database.validationResultsDao()).update(expectedSite.lastResult!!)
- }
-
- private fun fillInModel() = viewModel.apply {
- name.value = "Hello There"
- url.value = "https://www.hellothere.com"
- timeout.value = 30000
- validationMode.value = JAVASCRIPT
- validationSearchTerm.value = null
- validationScript.value = "throw 'Oh no!'"
- checkIntervalValue.value = 24
- checkIntervalUnit.value = 60000
- tags.value = "one,two"
- retryPolicyTimes.value = 5
- retryPolicyMinutes.value = 5
- headers.value = listOf(
- Header(2L, 1L, key = "Content-Type", value = "text/html"),
- Header(3L, 1L, key = "User-Agent", value = "NockNock")
- )
- }
-}
diff --git a/art/showcase5.png b/art/showcase5.png
deleted file mode 100644
index 6198f1f..0000000
Binary files a/art/showcase5.png and /dev/null differ
diff --git a/art/showcasemain.png b/art/showcasemain.png
new file mode 100644
index 0000000..3d6a0c6
Binary files /dev/null and b/art/showcasemain.png differ
diff --git a/build.gradle b/build.gradle
index 2d8e462..75c86f6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,32 +1,24 @@
-apply from: './dependencies.gradle'
-apply from: './versionsPlugin.gradle'
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- apply from: './dependencies.gradle'
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.1.3'
- repositories {
- google()
- jcenter()
- maven { url 'https://maven.fabric.io/public' }
- }
- 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
- classpath 'io.fabric.tools:gradle:' + versions.fabricPlugin
- classpath 'com.google.gms:google-services:' + versions.googleServices
- }
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
}
allprojects {
- repositories {
- google()
- jcenter()
- maven { url "https://jitpack.io" }
- }
-
- tasks.withType(Javadoc).all {
- enabled = false
- }
+ repositories {
+ jcenter()
+ maven { url "https://jitpack.io" }
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
}
diff --git a/common/.gitignore b/common/.gitignore
deleted file mode 100644
index 796b96d..0000000
--- a/common/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
diff --git a/common/build.gradle b/common/build.gradle
deleted file mode 100644
index 5004349..0000000
--- a/common/build.gradle
+++ /dev/null
@@ -1,44 +0,0 @@
-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
- }
-
- packagingOptions {
- exclude 'META-INF/atomicfu.kotlin_module'
- }
-
- // For Mozilla Rhino
- lintOptions {
- abortOnError false
- }
-}
-
-dependencies {
- implementation 'androidx.annotation:annotation:' + versions.androidxAnnotations
- api 'androidx.lifecycle:lifecycle-extensions:' + versions.lifecycle
-
- 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 'org.koin:koin-android:' + versions.koin
- implementation 'org.mozilla:rhino:' + versions.rhino
-
- api 'com.afollestad:rxkprefs:' + versions.rxkPrefs
- api "io.reactivex.rxjava2:rxjava:" + versions.rxJava
-
- testImplementation 'junit:junit:' + versions.junit
- testImplementation 'com.google.truth:truth:' + versions.truth
- testImplementation 'androidx.arch.core:core-testing:' + versions.archTesting
-}
-
-apply from: '../spotless.gradle'
\ No newline at end of file
diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml
deleted file mode 100644
index d67cd60..0000000
--- a/common/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/CommonModule.kt b/common/src/main/java/com/afollestad/nocknock/utilities/CommonModule.kt
deleted file mode 100644
index 49ceb52..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/CommonModule.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities
-
-import com.afollestad.nocknock.utilities.Qualifiers.IO_DISPATCHER
-import com.afollestad.nocknock.utilities.Qualifiers.MAIN_DISPATCHER
-import com.afollestad.nocknock.utilities.providers.RealStringProvider
-import com.afollestad.nocknock.utilities.providers.StringProvider
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.Dispatchers
-import org.koin.dsl.module.module
-
-object Qualifiers {
- const val MAIN_DISPATCHER = "main_dispatcher"
- const val IO_DISPATCHER = "io_dispatcher"
-}
-
-/** @author Aidan Follestad (@afollestad) */
-val commonModule = module {
-
- factory(name = MAIN_DISPATCHER) { Dispatchers.Main }
-
- factory(name = IO_DISPATCHER) { Dispatchers.IO }
-
- factory { RealStringProvider(get()) } bind StringProvider::class
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/ext/AnimationExt.kt b/common/src/main/java/com/afollestad/nocknock/utilities/ext/AnimationExt.kt
deleted file mode 100644
index 171a5cd..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/ext/AnimationExt.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.ext
-
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.view.View
-
-fun Animator.onEnd(cb: () -> Unit) {
- addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- super.onAnimationEnd(animation)
- cb()
- }
- })
-}
-
-fun View.animateRotation(
- loop: Boolean = true,
- firstPass: Boolean = true,
- durationPerRotation: Long = 1000,
- degreesPerRotation: Float = 360f
-) {
- if (firstPass) {
- animate().cancel()
- }
- animate()
- .rotationBy(degreesPerRotation)
- .setDuration(durationPerRotation)
- .setListener(object : AnimatorListenerAdapter() {
-
- var isCancelled = false
-
- override fun onAnimationCancel(animation: Animator?) {
- super.onAnimationCancel(animation)
- isCancelled = true
- }
-
- override fun onAnimationEnd(animation: Animator) {
- super.onAnimationEnd(animation)
- if (loop && !isCancelled) {
- animateRotation(loop = true, firstPass = false)
- }
- }
- })
- .start()
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/ext/ContextExt.kt b/common/src/main/java/com/afollestad/nocknock/utilities/ext/ContextExt.kt
deleted file mode 100644
index 4080195..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/ext/ContextExt.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.ext
-
-import android.content.Context
-
-/** @author Aidan Follestad (@afollestad) */
-inline fun Context.systemService(name: String): T {
- return getSystemService(name) as T
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/ext/DateExt.kt b/common/src/main/java/com/afollestad/nocknock/utilities/ext/DateExt.kt
deleted file mode 100644
index 6ef6e7f..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/ext/DateExt.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.ext
-
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
-
-fun Long.formatDate(): String {
- if (this <= 0) {
- return "(None)"
- }
- val df = SimpleDateFormat("MMMM d, h:mm a", Locale.getDefault())
- return df.format(Date(this))
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/ext/StringExt.kt b/common/src/main/java/com/afollestad/nocknock/utilities/ext/StringExt.kt
deleted file mode 100644
index 1256a27..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/ext/StringExt.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.ext
-
-import android.net.Uri
-
-fun String.toUri() = Uri.parse(this)!!
-
-fun String?.isNotNullOrEmpty(): Boolean {
- if (this == null || this == "null") {
- return false
- }
- return !isNullOrEmpty()
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/ext/TimeExt.kt b/common/src/main/java/com/afollestad/nocknock/utilities/ext/TimeExt.kt
deleted file mode 100644
index a7c65e2..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/ext/TimeExt.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.ext
-
-import kotlin.math.ceil
-
-const val SECOND: Long = 1000
-const val MINUTE = SECOND * 60
-const val HOUR = MINUTE * 60
-const val DAY = HOUR * 24
-const val WEEK = DAY * 7
-const val MONTH = WEEK * 4
-
-fun Long.timeString() = when {
- this <= 0 -> "??"
- this >= MONTH ->
- "${ceil((this.toFloat() / MONTH.toFloat()).toDouble()).toInt()}mo"
- this >= WEEK ->
- "${ceil((this.toFloat() / WEEK.toFloat()).toDouble()).toInt()}w"
- this >= DAY ->
- "${ceil((this.toFloat() / DAY.toFloat()).toDouble()).toInt()}d"
- this >= HOUR ->
- "${ceil((this.toFloat() / HOUR.toFloat()).toDouble()).toInt()}h"
- this >= MINUTE -> {
- val result = "${ceil((this.toFloat() / MINUTE.toFloat()).toDouble()).toInt()}m"
- if (result == "60m") {
- "1h"
- } else {
- result
- }
- }
- else -> "<1m"
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/ext/ViewExt.kt b/common/src/main/java/com/afollestad/nocknock/utilities/ext/ViewExt.kt
deleted file mode 100644
index 389de77..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/ext/ViewExt.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.ext
-
-import android.text.Editable
-import android.text.TextWatcher
-import android.widget.EditText
-import androidx.annotation.IntRange
-import kotlin.math.min
-
-fun EditText.setTextAndMaintainSelection(text: CharSequence?) {
- if (text == null) {
- setText("")
- return
- }
-
- val formerStart = min(selectionStart, text.length)
- val formerEnd = min(selectionEnd, text.length)
- setText(text)
- if (formerEnd <= formerStart) {
- setSelection(formerStart)
- } else {
- setSelection(formerStart, formerEnd)
- }
-}
-
-fun EditText.onTextChanged(
- @IntRange(from = 0, to = 10000) debounce: Int = 0,
- cb: (String) -> Unit
-) {
- addTextChangedListener(object : TextWatcher {
- val callbackRunner = Runnable {
- cb(text.trim().toString())
- }
-
- override fun afterTextChanged(s: Editable?) = Unit
-
- override fun beforeTextChanged(
- s: CharSequence,
- start: Int,
- count: Int,
- after: Int
- ) = Unit
-
- override fun onTextChanged(
- s: CharSequence,
- start: Int,
- before: Int,
- count: Int
- ) {
- removeCallbacks(callbackRunner)
- if (debounce == 0) {
- callbackRunner.run()
- } else {
- postDelayed(callbackRunner, debounce.toLong())
- }
- }
- })
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/js/JavaScript.kt b/common/src/main/java/com/afollestad/nocknock/utilities/js/JavaScript.kt
deleted file mode 100644
index cb94693..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/js/JavaScript.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.js
-
-import org.mozilla.javascript.Context
-import org.mozilla.javascript.EvaluatorException
-import org.mozilla.javascript.Function
-
-/** @author Aidan Follestad (@afollestad) */
-object JavaScript {
-
- fun eval(
- code: String,
- response: String
- ): String? {
- try {
- val 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
- val rhino = Context.enter()
-
- // Turn off optimization to make Rhino Android compatible
- rhino.optimizationLevel = -1
- try {
- val 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
- val jsFunction = scope.get("validate", scope) as Function
-
- // Call the function with params
- val jsResult = jsFunction.call(rhino, scope, scope, arrayOf(response))
-
- // Parse the jsResult object to a String
- val result = Context.toString(jsResult)
-
- val success = result != null && result == "true"
- var message = "The script returned a value other than true!"
- if (!success && result != null && result != "false") {
- message = if (result == "undefined") {
- "The script did not return or throw anything!"
- } else {
- result
- }
- }
-
- return if (!success) message else null
- } finally {
- Context.exit()
- }
- } catch (e: EvaluatorException) {
- return e.message
- }
- }
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/livedata/Distinct.kt b/common/src/main/java/com/afollestad/nocknock/utilities/livedata/Distinct.kt
deleted file mode 100644
index d6a26d2..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/livedata/Distinct.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.livedata
-
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MediatorLiveData
-import androidx.lifecycle.Observer
-
-/** @author Aidan Follestad (@afollestad) */
-class DistinctLiveData(source1: LiveData) : MediatorLiveData() {
-
- private var isInitialized = false
- private var lastValue: T? = null
-
- init {
- super.addSource(source1) {
- if (!isInitialized) {
- value = it
- isInitialized = true
- lastValue = it
- } else if (lastValue != it) {
- value = it
- lastValue = it
- }
- }
- }
-
- override fun addSource(
- source: LiveData,
- onChanged: Observer
- ) {
- throw UnsupportedOperationException()
- }
-
- override fun removeSource(toRemote: LiveData) {
- throw UnsupportedOperationException()
- }
-}
-
-fun LiveData.distinct(): MediatorLiveData = DistinctLiveData(this)
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/livedata/LiveDataExt.kt b/common/src/main/java/com/afollestad/nocknock/utilities/livedata/LiveDataExt.kt
deleted file mode 100644
index c74ed4d..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/livedata/LiveDataExt.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.livedata
-
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.Transformations
-
-fun LiveData.map(mapper: (X) -> Y) =
- Transformations.map(this, mapper)!!
-
-//fun LiveData.switchMap(mapper: (X) -> LiveData) =
-// Transformations.switchMap(this, mapper)!!
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/livedata/TestLiveData.kt b/common/src/main/java/com/afollestad/nocknock/utilities/livedata/TestLiveData.kt
deleted file mode 100644
index 7b94e95..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/livedata/TestLiveData.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.livedata
-
-import androidx.annotation.CheckResult
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.Observer
-
-/** @author Aidan Follestad (@afollestad) */
-class TestLiveData(data: LiveData) {
-
- private val receivedValues = mutableListOf()
- private val observer = Observer { emission ->
- emission?.let { receivedValues.add(it) }
- }
-
- init {
- data.observeForever(observer)
- }
-
- fun assertNoValues() {
- if (receivedValues.isNotEmpty()) {
- throw AssertionError("Expected no values, but got: $receivedValues")
- }
- }
-
- fun assertValues(vararg assertValues: T) {
- val assertList = assertValues.toList()
- if (!assertList.contentEquals(receivedValues)) {
- throw AssertionError("Expected $assertList\n\t\tBut got: $receivedValues")
- }
- receivedValues.clear()
- }
-
- @CheckResult fun values(): List = receivedValues
-
- private fun List.contentEquals(other: List): Boolean {
- if (this.size != other.size) {
- return false
- }
- for ((index, value) in this.withIndex()) {
- if (other[index] != value) {
- return false
- }
- }
- return true
- }
-}
-
-@CheckResult fun LiveData.test() = TestLiveData(this)
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/livedata/Zip.kt b/common/src/main/java/com/afollestad/nocknock/utilities/livedata/Zip.kt
deleted file mode 100644
index 739fa5b..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/livedata/Zip.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.livedata
-
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MediatorLiveData
-import androidx.lifecycle.Observer
-
-typealias Zipper = (T, K) -> R
-
-/** @author Aidan Follestad (@afollestad) */
-class ZipLiveData(
- source1: LiveData,
- source2: LiveData,
- private val distinctUntilChanged: Boolean,
- private val resetAfterEmission: Boolean,
- private val zipper: Zipper
-) : MediatorLiveData() {
-
- private var data1: T? = null
- private var data2: K? = null
- private var lastNotified: R? = null
-
- init {
- super.addSource(source1) {
- if (data1 == it) return@addSource
- data1 = it
- maybeNotify()
- }
- super.addSource(source2) {
- if (data2 == it) return@addSource
- data2 = it
- maybeNotify()
- }
- }
-
- private fun maybeNotify() {
- if (data1 != null && data2 != null) {
- val zippedUp = zipper(data1!!, data2!!)
-
- if (!distinctUntilChanged || zippedUp != lastNotified) {
- value = zippedUp
- lastNotified = zippedUp
-
- if (resetAfterEmission) {
- data1 = null
- data2 = null
- }
- }
- }
- }
-
- override fun addSource(
- source: LiveData,
- onChanged: Observer
- ) {
- throw UnsupportedOperationException()
- }
-
- override fun removeSource(toRemote: LiveData) {
- throw UnsupportedOperationException()
- }
-}
-
-fun zip(
- source1: LiveData,
- source2: LiveData,
- distinctUntilChanged: Boolean = true,
- resetAfterEmission: Boolean = false,
- zipper: Zipper
-) = ZipLiveData(
- source1 = source1,
- source2 = source2,
- distinctUntilChanged = distinctUntilChanged,
- resetAfterEmission = resetAfterEmission,
- zipper = zipper
-)
-
-fun zip(
- source1: LiveData,
- source2: LiveData,
- distinctUntilChanged: Boolean = true,
- resetAfterEmission: Boolean = false
-) = zip(
- source1 = source1,
- source2 = source2,
- distinctUntilChanged = distinctUntilChanged,
- resetAfterEmission = resetAfterEmission,
- zipper = { left, right -> Pair(left, right) })
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/providers/BundleProvider.kt b/common/src/main/java/com/afollestad/nocknock/utilities/providers/BundleProvider.kt
deleted file mode 100644
index 27894fe..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/providers/BundleProvider.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.providers
-
-import android.os.PersistableBundle
-
-interface IBundle {
- fun putLong(
- key: String,
- value: Long
- )
-}
-
-typealias IBundler = IBundle.() -> Unit
-
-/** @author Aidan Follestad (@afollestad) */
-interface BundleProvider {
-
- fun createPersistable(bundler: IBundle.() -> Unit): PersistableBundle
-}
-
-/** @author Aidan Follestad (@afollestad) */
-class RealBundleProvider : BundleProvider {
-
- override fun createPersistable(bundler: IBundler): PersistableBundle {
- val realBundle = PersistableBundle()
- bundler(object : IBundle {
- override fun putLong(
- key: String,
- value: Long
- ) = realBundle.putLong(key, value)
- })
- return realBundle
- }
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/providers/IntentProvider.kt b/common/src/main/java/com/afollestad/nocknock/utilities/providers/IntentProvider.kt
deleted file mode 100644
index 773243f..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/providers/IntentProvider.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.providers
-
-import android.app.PendingIntent
-import android.app.PendingIntent.FLAG_CANCEL_CURRENT
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import java.io.Serializable
-
-/** @author Aidan Follestad (@afollestad) */
-interface CanNotifyModel : Serializable {
-
- fun notifyId(): Int
-
- fun notifyName(): String
-
- fun notifyTag(): String
-
- fun notifyDescription(): String?
-}
-
-/** @author Aidan Follestad (@afollestad) */
-interface IntentProvider {
-
- fun createFilter(vararg actions: String): IntentFilter
-
- fun getPendingIntentForViewSite(
- model: CanNotifyModel
- ): PendingIntent
-}
-
-/** @author Aidan Follestad (@afollestad) */
-class RealIntentProvider(
- private val context: Context,
- private val mainActivityClass: Class<*>
-) : IntentProvider {
-
- companion object {
- const val BASE_NOTIFICATION_REQUEST_CODE = 40
- const val KEY_VIEW_NOTIFICATION_MODEL = "model"
- }
-
- override fun createFilter(vararg actions: String) = IntentFilter().apply {
- actions.forEach { addAction(it) }
- }
-
- override fun getPendingIntentForViewSite(model: CanNotifyModel): PendingIntent {
- val openIntent = getIntentForViewSite(model)
- return PendingIntent.getActivity(
- context,
- BASE_NOTIFICATION_REQUEST_CODE + model.notifyId(),
- openIntent,
- FLAG_CANCEL_CURRENT
- )
- }
-
- private fun getIntentForViewSite(model: CanNotifyModel) =
- Intent(context, mainActivityClass).apply {
- putExtra(KEY_VIEW_NOTIFICATION_MODEL, model)
- }
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/providers/JobInfoProvider.kt b/common/src/main/java/com/afollestad/nocknock/utilities/providers/JobInfoProvider.kt
deleted file mode 100644
index 0b59425..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/providers/JobInfoProvider.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.providers
-
-import android.app.job.JobInfo
-import android.app.job.JobInfo.NETWORK_TYPE_ANY
-import android.app.job.JobInfo.NETWORK_TYPE_UNMETERED
-import android.content.ComponentName
-import android.content.Context
-import android.os.PersistableBundle
-
-interface JobInfoProvider {
-
- fun createCheckJob(
- id: Int,
- onlyUnmeteredNetwork: Boolean,
- delayMs: Long,
- extras: PersistableBundle,
- target: Class<*>
- ): JobInfo
-}
-
-class RealJobInfoProvider(
- private val context: Context
-) : JobInfoProvider {
-
- // Note: 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 the app.
- override fun createCheckJob(
- id: Int,
- onlyUnmeteredNetwork: Boolean,
- delayMs: Long,
- extras: PersistableBundle,
- target: Class<*>
- ): JobInfo {
- val component = ComponentName(context, target)
- val networkType = if (onlyUnmeteredNetwork) {
- NETWORK_TYPE_UNMETERED
- } else {
- NETWORK_TYPE_ANY
- }
- return JobInfo.Builder(id, component)
- .setRequiredNetworkType(networkType)
- .setMinimumLatency(delayMs)
- .setExtras(extras)
- .build()
- }
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/providers/NotificationChannelProvider.kt b/common/src/main/java/com/afollestad/nocknock/utilities/providers/NotificationChannelProvider.kt
deleted file mode 100644
index e31ef04..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/providers/NotificationChannelProvider.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.providers
-
-import android.annotation.TargetApi
-import android.app.NotificationChannel
-import android.os.Build.VERSION_CODES
-
-/** @author Aidan Follestad (@afollestad) */
-interface NotificationChannelProvider {
-
- /** @return null if the device doesn't have Android O. */
- fun create(
- id: String,
- title: String,
- description: String,
- importance: Int
- ): NotificationChannel?
-}
-
-/** @author Aidan Follestad (@afollestad) */
-class RealNotificationChannelProvider(
- private val sdkProvider: SdkProvider
-) : NotificationChannelProvider {
-
- @TargetApi(VERSION_CODES.O)
- override fun create(
- id: String,
- title: String,
- description: String,
- importance: Int
- ): NotificationChannel? {
- if (!sdkProvider.hasOreo()) {
- return null
- }
- return NotificationChannel(id, title, importance)
- .apply {
- this.description = description
- }
- }
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/providers/NotificationProvider.kt b/common/src/main/java/com/afollestad/nocknock/utilities/providers/NotificationProvider.kt
deleted file mode 100644
index 391fcb4..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/providers/NotificationProvider.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.providers
-
-import android.app.Notification
-import android.app.PendingIntent
-import android.content.Context
-import android.graphics.Bitmap
-import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationCompat.BigTextStyle
-import androidx.core.app.NotificationCompat.DEFAULT_VIBRATE
-
-/** @author Aidan Follestad (@afollestad) */
-interface NotificationProvider {
-
- fun create(
- channelId: String,
- title: String,
- content: String,
- intent: PendingIntent,
- smallIcon: Int,
- largeIcon: Bitmap? = null
- ): Notification
-}
-
-/** @author Aidan Follestad (@afollestad) */
-class RealNotificationProvider(
- private val context: Context
-) : NotificationProvider {
-
- override fun create(
- channelId: String,
- title: String,
- content: String,
- intent: PendingIntent,
- smallIcon: Int,
- largeIcon: Bitmap?
- ): Notification {
- return NotificationCompat.Builder(context, channelId)
- .setContentTitle(title)
- .setContentText(content)
- .setContentIntent(intent)
- .setSmallIcon(smallIcon)
- .setLargeIcon(largeIcon)
- .setAutoCancel(true)
- .setDefaults(DEFAULT_VIBRATE)
- .setStyle(
- BigTextStyle()
- .bigText(content)
- )
- .build()
- }
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/providers/SdkProvider.kt b/common/src/main/java/com/afollestad/nocknock/utilities/providers/SdkProvider.kt
deleted file mode 100644
index a8ec0a9..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/providers/SdkProvider.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.providers
-
-import android.os.Build.VERSION.SDK_INT
-import android.os.Build.VERSION_CODES.O
-
-/** @author Aidan Follestad (@afollestad) */
-interface SdkProvider {
-
- fun hasOreo(): Boolean
-}
-
-/** @author Aidan Follestad (@afollestad) */
-class RealSdkProvider : SdkProvider {
-
- override fun hasOreo() = SDK_INT >= O
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/providers/StringProvider.kt b/common/src/main/java/com/afollestad/nocknock/utilities/providers/StringProvider.kt
deleted file mode 100644
index 1d24030..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/providers/StringProvider.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.providers
-
-import android.content.Context
-import androidx.annotation.StringRes
-
-/** @author Aidan Follestad (@afollestad) */
-interface StringProvider {
-
- fun get(@StringRes res: Int): String
-}
-
-/** @author Aidan Follestad (@afollestad) */
-class RealStringProvider(
- private val context: Context
-) : StringProvider {
-
- override fun get(res: Int): String {
- return context.resources.getString(res)
- }
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/rx/RxLifecycleExt.kt b/common/src/main/java/com/afollestad/nocknock/utilities/rx/RxLifecycleExt.kt
deleted file mode 100644
index a306aad..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/rx/RxLifecycleExt.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-@file:Suppress("unused")
-
-package com.afollestad.nocknock.utilities.rx
-
-import androidx.lifecycle.Lifecycle.Event.ON_DESTROY
-import androidx.lifecycle.Lifecycle.State.DESTROYED
-import androidx.lifecycle.LifecycleObserver
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.OnLifecycleEvent
-import io.reactivex.disposables.Disposable
-
-/** @author Aidan Follestad (afollestad) */
-class LifecycleAwareDisposable(
- private val disposable: Disposable
-) : LifecycleObserver {
-
- @OnLifecycleEvent(ON_DESTROY)
- fun dispose() = disposable.dispose()
-}
-
-/**
- * Wraps [disposable] so that it is disposed of when the receiving [LifecycleOwner]
- * is destroyed.
- *
- * @author Aidan Follestad (afollestad)
- */
-fun LifecycleOwner.ownRx(disposable: Disposable) {
- if (this.lifecycle.currentState == DESTROYED) {
- disposable.dispose()
- return
- }
- this.lifecycle.addObserver(LifecycleAwareDisposable(disposable))
-}
-
-/**
- * Attaches the receiving [Disposable] so that it is disposed of when [lifecycleOwner]
- * is destroyed.
- *
- * @author Aidan Follestad (afollestad)
- */
-fun Disposable.attachLifecycle(lifecycleOwner: LifecycleOwner) {
- lifecycleOwner.ownRx(this)
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/ui/ContextExt.kt b/common/src/main/java/com/afollestad/nocknock/utilities/ui/ContextExt.kt
deleted file mode 100644
index 5b09b3d..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/ui/ContextExt.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.ui
-
-import android.content.Context
-import android.widget.Toast
-
-private var toast: Toast? = null
-
-/** Shows a toast in the receiving context, cancelling any previous. */
-fun Context.toast(message: Int) {
- toast?.cancel()
- toast = Toast.makeText(this, message, Toast.LENGTH_LONG)
- .apply {
- show()
- }
-}
diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/ui/DebouncedOnClickListener.kt b/common/src/main/java/com/afollestad/nocknock/utilities/ui/DebouncedOnClickListener.kt
deleted file mode 100644
index bfb7a86..0000000
--- a/common/src/main/java/com/afollestad/nocknock/utilities/ui/DebouncedOnClickListener.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.ui
-
-import android.view.View
-import java.lang.System.currentTimeMillis
-
-private const val DEFAULT_DEBOUNCE_INTERVAL = 750L
-
-/** @author Aidan Follestad (@afollestad) */
-abstract class DebouncedOnClickListener(
- private val delayBetweenClicks: Long = DEFAULT_DEBOUNCE_INTERVAL
-) : View.OnClickListener {
-
- private var lastClickTimestamp = -1L
-
- @Deprecated(
- message = "onDebouncedClick should be overridden instead.",
- replaceWith = ReplaceWith("onDebouncedClick(v)")
- )
- override fun onClick(v: View) {
- val now = currentTimeMillis()
- if (lastClickTimestamp == -1L || now >= (lastClickTimestamp + delayBetweenClicks)) {
- onDebouncedClick(v)
- }
- lastClickTimestamp = now
- }
-
- abstract fun onDebouncedClick(v: View)
-}
-
-/** @author Aidan Follestad (@afollestad) */
-fun View.onDebouncedClick(
- delayBetweenClicks: Long = DEFAULT_DEBOUNCE_INTERVAL,
- click: (view: View) -> Unit
-) {
- setOnClickListener(object : DebouncedOnClickListener(delayBetweenClicks) {
- override fun onDebouncedClick(v: View) {
- click(v)
- }
- })
-}
diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml
deleted file mode 100644
index fda664e..0000000
--- a/common/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
- Checks Disabled
- Automatic Checks Disabled
-
diff --git a/common/src/test/java/com/afollestad/nocknock/utilities/livedata/DistinctTest.kt b/common/src/test/java/com/afollestad/nocknock/utilities/livedata/DistinctTest.kt
deleted file mode 100644
index 465a085..0000000
--- a/common/src/test/java/com/afollestad/nocknock/utilities/livedata/DistinctTest.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.livedata
-
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import androidx.lifecycle.MutableLiveData
-import org.junit.Rule
-import org.junit.Test
-
-/** @author Aidan Follestad (@afollestad) */
-class DistinctTest {
-
- @Rule @JvmField val rule = InstantTaskExecutorRule()
-
- @Test fun filterLastValues() {
- val data = MutableLiveData()
- val distinct = data.distinct()
- .test()
-
- data.postValue("Hello")
- data.postValue("Hello")
-
- data.postValue("Hi")
- data.postValue("Hi")
-
- data.postValue("Hello")
-
- distinct.assertValues(
- "Hello",
- "Hi",
- "Hello"
- )
- }
-}
diff --git a/common/src/test/java/com/afollestad/nocknock/utilities/livedata/ZipTest.kt b/common/src/test/java/com/afollestad/nocknock/utilities/livedata/ZipTest.kt
deleted file mode 100644
index 0cc57da..0000000
--- a/common/src/test/java/com/afollestad/nocknock/utilities/livedata/ZipTest.kt
+++ /dev/null
@@ -1,123 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.utilities.livedata
-
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import androidx.lifecycle.MutableLiveData
-import org.junit.Rule
-import org.junit.Test
-
-/** @author Aidan Follestad (@afollestad) */
-class ZipTest {
-
- @Rule @JvmField val rule = InstantTaskExecutorRule()
-
- @Test fun test_withDistinct() {
- val data1 = MutableLiveData()
- val data2 = MutableLiveData()
- val zipped = zip(data1, data2, true)
- .test()
-
- data1.postValue("Hello")
- data2.postValue(24)
- zipped.assertValues(Pair("Hello", 24))
-
- data1.postValue("Hello")
- data2.postValue(24)
- zipped.assertNoValues()
- }
-
- @Test fun test_noDistinct() {
- val data1 = MutableLiveData()
- val data2 = MutableLiveData()
- val zipped = zip(data1, data2, false)
- .test()
-
- data1.postValue("Hello")
- data2.postValue(24)
- zipped.assertValues(Pair("Hello", 24))
-
- data1.postValue("Hi")
- data2.postValue(24)
- zipped.assertValues(Pair("Hi", 24))
- }
-
- @Test fun test_noDistinct_resetAfterEmission() {
- val data1 = MutableLiveData()
- val data2 = MutableLiveData()
- val zipped = zip(data1, data2, false, true)
- .test()
-
- data1.postValue("Hello")
- data2.postValue(24)
- zipped.assertValues(Pair("Hello", 24))
-
- data1.postValue("Hi")
- data2.postValue(50)
- zipped.assertValues(Pair("Hi", 50))
- }
-
- @Test fun test_withDistinct_customZipper() {
- val data1 = MutableLiveData()
- val data2 = MutableLiveData()
- val zipped = zip(data1, data2, true,
- zipper = { left, right ->
- "$left $right"
- }).test()
-
- data1.postValue("Hello")
- data2.postValue(24)
- zipped.assertValues("Hello 24")
-
- data1.postValue("Hello")
- data2.postValue(24)
- zipped.assertNoValues()
- }
-
- @Test fun test_noDistinct_customZipper() {
- val data1 = MutableLiveData()
- val data2 = MutableLiveData()
- val zipped = zip(data1, data2, false,
- zipper = { left, right ->
- "$left $right"
- }).test()
-
- data1.postValue("Hello")
- data2.postValue(24)
- zipped.assertValues("Hello 24")
-
- data1.postValue("Hi")
- data2.postValue(24)
- zipped.assertValues("Hi 24")
- }
-
- @Test fun test_noDistinct_customZipper_resetAfterEmission() {
- val data1 = MutableLiveData()
- val data2 = MutableLiveData()
- val zipped = zip(data1, data2, false, true,
- zipper = { left, right ->
- "$left $right"
- }).test()
-
- data1.postValue("Hello")
- data2.postValue(24)
- zipped.assertValues("Hello 24")
-
- data1.postValue("Hi")
- data2.postValue(50)
- zipped.assertValues("Hi 50")
- }
-}
diff --git a/data/.gitignore b/data/.gitignore
deleted file mode 100644
index 796b96d..0000000
--- a/data/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
diff --git a/data/build.gradle b/data/build.gradle
deleted file mode 100644
index e413d4c..0000000
--- a/data/build.gradle
+++ /dev/null
@@ -1,37 +0,0 @@
-apply from: '../dependencies.gradle'
-apply plugin: 'com.android.library'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-kapt'
-
-android {
- compileSdkVersion versions.compileSdk
-
- defaultConfig {
- minSdkVersion versions.minSdk
- targetSdkVersion versions.compileSdk
- versionCode versions.publishVersionCode
- versionName versions.publishVersion
-
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- }
-
- packagingOptions {
- exclude 'META-INF/atomicfu.kotlin_module'
- }
-}
-
-dependencies {
- implementation project(':common')
-
- implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin
-
- api 'androidx.room:room-runtime:' + versions.room
- kapt 'androidx.room:room-compiler:' + versions.room
-
- androidTestImplementation 'androidx.test:runner:' + versions.androidxTestRunner
- androidTestImplementation 'androidx.test:rules:' + versions.androidxTestRunner
- androidTestImplementation 'androidx.test:core:' + versions.androidxTest
- androidTestImplementation 'com.google.truth:truth:' + versions.truth
-}
-
-apply from: '../spotless.gradle'
\ No newline at end of file
diff --git a/data/src/androidTest/AndroidManifest.xml b/data/src/androidTest/AndroidManifest.xml
deleted file mode 100644
index c83c51d..0000000
--- a/data/src/androidTest/AndroidManifest.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
diff --git a/data/src/androidTest/java/com/afollestad/nocknock/data/AppDatabaseTest.kt b/data/src/androidTest/java/com/afollestad/nocknock/data/AppDatabaseTest.kt
deleted file mode 100644
index e7d4400..0000000
--- a/data/src/androidTest/java/com/afollestad/nocknock/data/AppDatabaseTest.kt
+++ /dev/null
@@ -1,554 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-@file:Suppress("RemoveEmptyPrimaryConstructor")
-
-package com.afollestad.nocknock.data
-
-import android.content.Context
-import androidx.room.Room.inMemoryDatabaseBuilder
-import androidx.test.core.app.ApplicationProvider.getApplicationContext
-import androidx.test.runner.AndroidJUnit4
-import com.afollestad.nocknock.data.model.Header
-import com.afollestad.nocknock.data.model.RetryPolicy
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.data.model.SiteSettings
-import com.afollestad.nocknock.data.model.Status.ERROR
-import com.afollestad.nocknock.data.model.Status.OK
-import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
-import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
-import com.afollestad.nocknock.data.model.ValidationResult
-import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import java.io.IOException
-import java.lang.System.currentTimeMillis
-
-/** @author Aidan Follestad (@afollestad) */
-@RunWith(AndroidJUnit4::class)
-class AppDatabaseTest() {
-
- private lateinit var db: AppDatabase
- private lateinit var sitesDao: SiteDao
- private lateinit var settingsDao: SiteSettingsDao
- private lateinit var resultsDao: ValidationResultsDao
- private lateinit var retryDao: RetryPolicyDao
- private lateinit var headerDao: HeaderDao
-
- @Before fun setup() {
- val context = getApplicationContext()
- db = inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
- sitesDao = db.siteDao()
- settingsDao = db.siteSettingsDao()
- resultsDao = db.validationResultsDao()
- retryDao = db.retryPolicyDao()
- headerDao = db.headerDao()
- }
-
- @After
- @Throws(IOException::class)
- fun destroy() = db.close()
-
- // SiteDao
-
- @Test fun site_insert_and_get_all() {
- val model1 = Site(
- name = "Test 1",
- url = "https://test1.com",
- tags = "",
- settings = null,
- lastResult = null,
- retryPolicy = null,
- headers = emptyList()
- )
- val newId1 = sitesDao.insert(model1)
- assertThat(newId1).isGreaterThan(0)
-
- val model2 = Site(
- name = "Test 2",
- url = "https://test2.com",
- tags = "",
- settings = null,
- lastResult = null,
- retryPolicy = null,
- headers = emptyList()
- )
- val newId2 = sitesDao.insert(model2)
- assertThat(newId2).isGreaterThan(newId1)
-
- val models = sitesDao.all()
- assertThat(models.size).isEqualTo(2)
- assertThat(models[0]).isEqualTo(model1.copy(id = newId1))
- assertThat(models[1]).isEqualTo(model2.copy(id = newId2))
- }
-
- @Test fun site_insert_and_get_one() {
- val model = Site(
- name = "Test",
- url = "https://test.com",
- tags = "",
- settings = null,
- lastResult = null,
- retryPolicy = null,
- headers = emptyList()
- )
- val newId = sitesDao.insert(model)
- assertThat(newId).isGreaterThan(0)
-
- val models = sitesDao.all()
- assertThat(models.single()).isEqualTo(model.copy(id = newId))
- }
-
- @Test fun site_insert_and_update() {
- val initialModel = Site(
- name = "Test 1",
- url = "https://test1.com",
- tags = "",
- settings = null,
- lastResult = null,
- retryPolicy = null,
- headers = emptyList()
- )
- val newId = sitesDao.insert(initialModel)
- assertThat(newId).isGreaterThan(0)
-
- val insertedModel = sitesDao.all()
- .single()
- val updatedModel = insertedModel.copy(
- name = "Test 2",
- url = "https://hi.com"
- )
- assertThat(sitesDao.update(updatedModel)).isEqualTo(1)
-
- val finalModel = sitesDao.all()
- .single()
- assertThat(finalModel).isNotEqualTo(initialModel.copy(id = newId))
- }
-
- @Test fun site_insert_and_delete() {
- val model1 = Site(
- name = "Test 1",
- url = "https://test1.com",
- tags = "",
- settings = null,
- lastResult = null,
- retryPolicy = null,
- headers = emptyList()
- )
- val newId1 = sitesDao.insert(model1)
- assertThat(newId1).isGreaterThan(0)
-
- val model2 = Site(
- name = "Test 2",
- url = "https://test2.com",
- tags = "",
- settings = null,
- lastResult = null,
- retryPolicy = null,
- headers = emptyList()
- )
- val newId2 = sitesDao.insert(model2)
- assertThat(newId2).isGreaterThan(newId1)
-
- val models1 = sitesDao.all()
- sitesDao.delete(models1[0])
-
- val models2 = sitesDao.all()
- assertThat(models2.single()).isEqualTo(models1[1])
- }
-
- // SiteSettingsDao
-
- @Test fun settings_insert_and_forSite() {
- val model = SiteSettings(
- siteId = 1,
- validationIntervalMs = 60000,
- validationMode = STATUS_CODE,
- validationArgs = null,
- disabled = false,
- networkTimeout = 10000,
- certificate = null
- )
- val newId = settingsDao.insert(model)
- assertThat(newId).isEqualTo(1)
-
- val finalModel = settingsDao.forSite(newId)
- .single()
- assertThat(finalModel).isEqualTo(model.copy(siteId = newId))
- }
-
- @Test fun settings_update() {
- settingsDao.insert(
- SiteSettings(
- siteId = 1,
- validationIntervalMs = 60000,
- validationMode = STATUS_CODE,
- validationArgs = null,
- disabled = false,
- networkTimeout = 10000,
- certificate = null
- )
- )
-
- val insertedModel = settingsDao.forSite(1)
- .single()
- val updatedModel = insertedModel.copy(
- validationIntervalMs = 10000,
- validationMode = TERM_SEARCH,
- validationArgs = "test",
- disabled = false,
- networkTimeout = 1000
- )
- assertThat(settingsDao.update(updatedModel)).isEqualTo(1)
-
- val finalModel = settingsDao.forSite(1)
- .single()
- assertThat(finalModel).isEqualTo(updatedModel)
- }
-
- @Test fun settings_delete() {
- settingsDao.insert(
- SiteSettings(
- siteId = 1,
- validationIntervalMs = 60000,
- validationMode = STATUS_CODE,
- validationArgs = null,
- disabled = false,
- networkTimeout = 10000,
- certificate = null
- )
- )
-
- val insertedModel = settingsDao.forSite(1)
- .single()
- settingsDao.delete(insertedModel)
- assertThat(settingsDao.forSite(1)).isEmpty()
- }
-
- // ValidationResultsDao
-
- @Test fun validation_insert_and_forSite() {
- val model = ValidationResult(
- siteId = 1,
- timestampMs = currentTimeMillis(),
- status = ERROR,
- reason = "Oh no"
- )
- val newId = resultsDao.insert(model)
- assertThat(newId).isEqualTo(1)
-
- val finalModel = resultsDao.forSite(newId)
- .single()
- assertThat(finalModel).isEqualTo(model.copy(siteId = newId))
- }
-
- @Test fun validation_update() {
- resultsDao.insert(
- ValidationResult(
- siteId = 1,
- timestampMs = currentTimeMillis(),
- status = ERROR,
- reason = "Oh no"
- )
- )
-
- val insertedModel = resultsDao.forSite(1)
- .single()
- val updatedModel = insertedModel.copy(
- timestampMs = currentTimeMillis() + 1000,
- status = OK,
- reason = null
- )
- assertThat(resultsDao.update(updatedModel)).isEqualTo(1)
-
- val finalModel = resultsDao.forSite(1)
- .single()
- assertThat(finalModel).isEqualTo(updatedModel)
- }
-
- @Test fun validation_delete() {
- resultsDao.insert(
- ValidationResult(
- siteId = 1,
- timestampMs = currentTimeMillis(),
- status = ERROR,
- reason = "Oh no"
- )
- )
-
- val insertedModel = resultsDao.forSite(1)
- .single()
- resultsDao.delete(insertedModel)
- assertThat(resultsDao.forSite(1)).isEmpty()
- }
-
- // RetryPolicyDao
-
- @Test fun retryPolicy_insert_and_forSite() {
- val model = RetryPolicy(
- siteId = 1,
- count = 3,
- minutes = 6
- )
- val newId = retryDao.insert(model)
- assertThat(newId).isEqualTo(1)
-
- val finalModel = retryDao.forSite(newId)
- .single()
- assertThat(finalModel).isEqualTo(model.copy(siteId = newId))
- }
-
- @Test fun retryPolicy_update() {
- retryDao.insert(
- RetryPolicy(
- siteId = 1,
- count = 3,
- minutes = 6
- )
- )
-
- val insertedModel = retryDao.forSite(1)
- .single()
- val updatedModel = insertedModel.copy(
- count = 4,
- minutes = 8
- )
- assertThat(retryDao.update(updatedModel)).isEqualTo(1)
-
- val finalModel = retryDao.forSite(1)
- .single()
- assertThat(finalModel).isEqualTo(updatedModel)
- }
-
- @Test fun retryPolicy_delete() {
- retryDao.insert(
- RetryPolicy(
- siteId = 1,
- count = 3,
- minutes = 6
- )
- )
-
- val insertedModel = retryDao.forSite(1)
- .single()
- retryDao.delete(insertedModel)
- assertThat(retryDao.forSite(1)).isEmpty()
- }
-
- // HeaderDao
-
- @Test fun headers_insert_and_forSite() {
- val models = listOf(
- Header(
- siteId = 1,
- key = "Name",
- value = "Aidan"
- ),
- Header(
- siteId = 1,
- key = "Born",
- value = "1995"
- )
- )
- val newIds = headerDao.insert(models)
- assertThat(newIds.first()).isEqualTo(1)
- assertThat(newIds.last()).isEqualTo(2)
-
- val finalModels = headerDao.forSite(1)
- assertThat(finalModels.first()).isEqualTo(models.first().copy(id = 1))
- assertThat(finalModels.last()).isEqualTo(models.last().copy(id = 2))
- }
-
- @Test fun headers_update() {
- val models = listOf(
- Header(
- siteId = 1,
- key = "Name",
- value = "Aidan"
- ),
- Header(
- siteId = 1,
- key = "Born",
- value = "1995"
- )
- )
- headerDao.insert(models)
-
- val insertedModel = headerDao.forSite(1)
- .last()
- val updatedModel = insertedModel.copy(
- key = "Test",
- value = "Hello"
- )
- assertThat(headerDao.update(updatedModel)).isEqualTo(1)
-
- val finalModels = headerDao.forSite(1)
- assertThat(finalModels.first()).isEqualTo(models.first().copy(id = 1))
- assertThat(finalModels.last()).isEqualTo(updatedModel)
- }
-
- @Test fun headers_delete() {
- val models = listOf(
- Header(
- siteId = 1,
- key = "Name",
- value = "Aidan"
- ),
- Header(
- siteId = 1,
- key = "Born",
- value = "1995"
- )
- )
- headerDao.insert(models)
-
- val insertedModels = headerDao.forSite(1)
- headerDao.delete(insertedModels)
- assertThat(headerDao.forSite(1)).isEmpty()
- }
-
- // Extension Methods
-
- @Test fun extension_put_and_allSites() {
- db.putSite(MOCK_MODEL_1)
- db.putSite(MOCK_MODEL_2)
- db.putSite(MOCK_MODEL_3)
-
- val allSites = db.allSites()
- assertThat(allSites.size).isEqualTo(3)
- assertThat(allSites[0]).isEqualTo(
- MOCK_MODEL_1.copy(
- headers = listOf(
- MOCK_MODEL_1.headers.first().copy(id = 1),
- MOCK_MODEL_1.headers.last().copy(id = 2)
- )
- )
- )
- assertThat(allSites[1]).isEqualTo(
- MOCK_MODEL_2.copy(
- headers = listOf(
- MOCK_MODEL_2.headers.first().copy(id = 3),
- MOCK_MODEL_2.headers.last().copy(id = 4)
- )
- )
- )
- assertThat(allSites[2]).isEqualTo(
- MOCK_MODEL_3.copy(
- headers = listOf(
- MOCK_MODEL_3.headers.first().copy(id = 5),
- MOCK_MODEL_3.headers.last().copy(id = 6)
- )
- )
- )
- }
-
- @Test fun extension_put_getSite() {
- db.putSite(MOCK_MODEL_1)
- db.putSite(MOCK_MODEL_2)
- db.putSite(MOCK_MODEL_3)
- val allSites = db.allSites()
-
- val site = db.getSite(2)
- assertThat(site).isEqualTo(allSites[1])
- }
-
- @Test fun extension_put_updateSite() {
- db.putSite(MOCK_MODEL_1)
- db.putSite(MOCK_MODEL_2)
- db.putSite(MOCK_MODEL_3)
- val modelToUpdate = db.allSites()[1]
-
- val updatedSettings = modelToUpdate.settings!!.copy(
- validationIntervalMs = 1,
- validationMode = JAVASCRIPT,
- validationArgs = "throw 'Hello World'",
- disabled = false,
- networkTimeout = 50
- )
- val updatedValidationResult = modelToUpdate.lastResult!!.copy(
- timestampMs = currentTimeMillis() + 10,
- status = ERROR,
- reason = "Oh no"
- )
- val updatedRetryPolicy = modelToUpdate.retryPolicy!!.copy(
- count = 4,
- minutes = 8
- )
- val updatedHeaders = listOf(
- modelToUpdate.headers.first().copy(
- id = 7,
- key = "One",
- value = "Hello"
- ),
- modelToUpdate.headers.last().copy(
- id = 8,
- key = "Two",
- value = "Hey"
- )
- )
- val updatedModel = modelToUpdate.copy(
- name = "Oijrfouhef",
- url = "https://iojfdfsdk.io",
- settings = updatedSettings,
- lastResult = updatedValidationResult,
- retryPolicy = updatedRetryPolicy,
- headers = updatedHeaders
- )
-
- db.updateSite(updatedModel)
-
- val finalSite = db.getSite(modelToUpdate.id)!!
- assertThat(finalSite.settings).isEqualTo(updatedSettings)
- assertThat(finalSite.lastResult).isEqualTo(updatedValidationResult)
- assertThat(finalSite.retryPolicy).isEqualTo(updatedRetryPolicy)
- assertThat(finalSite.headers.first()).isEqualTo(updatedHeaders.first())
- assertThat(finalSite.headers.last()).isEqualTo(updatedHeaders.last())
- assertThat(finalSite).isEqualTo(updatedModel)
- }
-
- @Test fun extension_put_and_deleteSite() {
- db.putSite(MOCK_MODEL_1)
- db.putSite(MOCK_MODEL_2)
- db.putSite(MOCK_MODEL_3)
- val allSites = db.allSites()
-
- db.deleteSite(allSites[1])
-
- val remainingSettings = settingsDao.all()
- assertThat(remainingSettings.size).isEqualTo(2)
- assertThat(remainingSettings[0]).isEqualTo(allSites[0].settings!!)
- assertThat(remainingSettings[1]).isEqualTo(allSites[2].settings!!)
-
- val remainingResults = resultsDao.all()
- assertThat(remainingResults.size).isEqualTo(2)
- assertThat(remainingResults[0]).isEqualTo(allSites[0].lastResult!!)
- assertThat(remainingResults[1]).isEqualTo(allSites[2].lastResult!!)
-
- val remainingRetryPolicies = retryDao.all()
- assertThat(remainingRetryPolicies.size).isEqualTo(2)
- assertThat(remainingRetryPolicies[0]).isEqualTo(allSites[0].retryPolicy!!)
- assertThat(remainingRetryPolicies[1]).isEqualTo(allSites[2].retryPolicy!!)
-
- val remainingHeaders = headerDao.all()
- assertThat(remainingHeaders.size).isEqualTo(4)
- assertThat(remainingHeaders[0]).isEqualTo(allSites[0].headers.first())
- assertThat(remainingHeaders[1]).isEqualTo(allSites[0].headers.last())
- assertThat(remainingHeaders[2]).isEqualTo(allSites[2].headers.first())
- assertThat(remainingHeaders[3]).isEqualTo(allSites[2].headers.last())
- }
-}
diff --git a/data/src/androidTest/java/com/afollestad/nocknock/data/TestUtil.kt b/data/src/androidTest/java/com/afollestad/nocknock/data/TestUtil.kt
deleted file mode 100644
index 2866b50..0000000
--- a/data/src/androidTest/java/com/afollestad/nocknock/data/TestUtil.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.data
-
-import com.afollestad.nocknock.data.model.Header
-import com.afollestad.nocknock.data.model.RetryPolicy
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.data.model.SiteSettings
-import com.afollestad.nocknock.data.model.Status
-import com.afollestad.nocknock.data.model.Status.OK
-import com.afollestad.nocknock.data.model.ValidationMode
-import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.data.model.ValidationResult
-import java.lang.System.currentTimeMillis
-
-fun fakeSettingsModel(
- id: Long,
- validationMode: ValidationMode = STATUS_CODE
-) = SiteSettings(
- siteId = id,
- validationIntervalMs = 600000,
- validationMode = validationMode,
- validationArgs = null,
- disabled = false,
- networkTimeout = 10000,
- certificate = null
-)
-
-fun fakeResultModel(
- id: Long,
- status: Status = OK,
- reason: String? = null
-) = ValidationResult(
- siteId = id,
- status = status,
- reason = reason,
- timestampMs = currentTimeMillis()
-)
-
-fun fakeRetryPolicy(
- id: Long,
- count: Int = 3,
- minutes: Int = 6
-) = RetryPolicy(
- siteId = id,
- count = count,
- minutes = minutes
-)
-
-fun fakeHeaders(siteId: Long) = listOf(
- Header(siteId = siteId, key = "Content-Type", value = "text/html"),
- Header(siteId = siteId, key = "User-Agent", value = "NockNock")
-)
-
-fun fakeModel(id: Long) = Site(
- id = id,
- name = "Test",
- url = "https://test.com",
- tags = "",
- settings = fakeSettingsModel(id),
- lastResult = fakeResultModel(id),
- retryPolicy = fakeRetryPolicy(id),
- headers = fakeHeaders(id)
-)
-
-val MOCK_MODEL_1 = fakeModel(1)
-val MOCK_MODEL_2 = fakeModel(2)
-val MOCK_MODEL_3 = fakeModel(3)
diff --git a/data/src/main/AndroidManifest.xml b/data/src/main/AndroidManifest.xml
deleted file mode 100644
index 465b811..0000000
--- a/data/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
diff --git a/data/src/main/java/com/afollestad/nocknock/data/AppDatabase.kt b/data/src/main/java/com/afollestad/nocknock/data/AppDatabase.kt
deleted file mode 100644
index 47687ba..0000000
--- a/data/src/main/java/com/afollestad/nocknock/data/AppDatabase.kt
+++ /dev/null
@@ -1,191 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.data
-
-import androidx.room.Database
-import androidx.room.RoomDatabase
-import androidx.room.TypeConverters
-import com.afollestad.nocknock.data.model.Converters
-import com.afollestad.nocknock.data.model.Header
-import com.afollestad.nocknock.data.model.RetryPolicy
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.data.model.SiteSettings
-import com.afollestad.nocknock.data.model.ValidationResult
-
-/** @author Aidan Follestad (@afollestad) */
-@Database(
- entities = [
- Header::class,
- RetryPolicy::class,
- ValidationResult::class,
- SiteSettings::class,
- Site::class
- ],
- version = 5,
- exportSchema = false
-)
-@TypeConverters(Converters::class)
-abstract class AppDatabase : RoomDatabase() {
-
- abstract fun siteDao(): SiteDao
-
- abstract fun siteSettingsDao(): SiteSettingsDao
-
- abstract fun validationResultsDao(): ValidationResultsDao
-
- abstract fun retryPolicyDao(): RetryPolicyDao
-
- abstract fun headerDao(): HeaderDao
-}
-
-/**
- * Gets all sites and maps their settings and last validation results.
- *
- * @author Aidan Follestad (@afollestad)
- */
-fun AppDatabase.allSites(): List {
- return siteDao().all()
- .map {
- val settings = siteSettingsDao().forSite(it.id)
- .single()
- val lastResult = validationResultsDao().forSite(it.id)
- .singleOrNull()
- val retryPolicy = retryPolicyDao().forSite(it.id)
- .singleOrNull()
- val headers = headerDao().forSite(it.id)
- return@map it.copy(
- settings = settings,
- lastResult = lastResult,
- retryPolicy = retryPolicy,
- headers = headers
- )
- }
-}
-
-/**
- * Gets a single site and maps its settings and last validation result.
- *
- * @author Aidan Follestad (@afollestad)
- */
-fun AppDatabase.getSite(id: Long): Site? {
- val result = siteDao().one(id)
- .singleOrNull() ?: return null
- val settings = siteSettingsDao().forSite(id)
- .single()
- val lastResult = validationResultsDao().forSite(id)
- .singleOrNull()
- val retryPolicy = retryPolicyDao().forSite(id)
- .singleOrNull()
- val headers = headerDao().forSite(id)
- return result.copy(
- settings = settings,
- lastResult = lastResult,
- retryPolicy = retryPolicy,
- headers = headers
- )
-}
-
-/**
- * Inserts a site along with its settings and last result into the database.
- *
- * @author Aidan Follestad (@afollestad)
- */
-fun AppDatabase.putSite(site: Site): Site {
- val settings = site.settings ?: throw IllegalArgumentException("Settings cannot be null.")
- val newId = siteDao().insert(site)
- val settingsWithSiteId = settings.copy(siteId = newId)
- val lastResultWithSiteId = site.lastResult?.copy(siteId = newId)
- val retryPolicyWithSiteId = site.retryPolicy?.copy(siteId = newId)
- val headersWithSiteId = site.headers.map { it.copy(siteId = newId) }
-
- siteSettingsDao().insert(settingsWithSiteId)
- lastResultWithSiteId?.let { validationResultsDao().insert(it) }
- retryPolicyWithSiteId?.let { retryPolicyDao().insert(it) }
- headerDao().insert(headersWithSiteId)
-
- return site.copy(
- id = newId,
- settings = settingsWithSiteId,
- lastResult = lastResultWithSiteId,
- retryPolicy = retryPolicyWithSiteId,
- headers = headersWithSiteId
- )
-}
-
-/**
- * Updates a site, along with its settings and last result, in the database.
- *
- * @author Aidan Follestad (@afollestad)
- */
-fun AppDatabase.updateSite(site: Site) {
- siteDao().update(site)
-
- val settings = site.settings?.copy(siteId = site.id)
- if (settings != null) {
- val existing = siteSettingsDao().forSite(site.id)
- .singleOrNull()
- if (existing != null) {
- siteSettingsDao().update(settings)
- } else {
- siteSettingsDao().insert(settings)
- }
- }
-
- val lastResult = site.lastResult?.copy(siteId = site.id)
- if (lastResult != null) {
- val existing = validationResultsDao().forSite(site.id)
- .singleOrNull()
- if (existing != null) {
- validationResultsDao().update(lastResult)
- } else {
- validationResultsDao().insert(lastResult)
- }
- }
-
- val retryPolicy = site.retryPolicy?.copy(siteId = site.id)
- if (retryPolicy != null) {
- val existing = retryPolicyDao().forSite(site.id)
- .singleOrNull()
- if (existing != null) {
- retryPolicyDao().update(retryPolicy)
- } else {
- retryPolicyDao().insert(retryPolicy)
- }
- }
-
- // Wipe existing headers
- headerDao().delete(headerDao().forSite(site.id))
- // Then add ones that still exist
- site.headers.forEach { header ->
- headerDao().insert(header.copy(id = 0, siteId = site.id))
- }
-}
-
-/**
- * Deletes a site along with its settings and last result from the database.
- *
- * @author Aidan Follestad (@afollestad)
- */
-fun AppDatabase.deleteSite(site: Site) {
- site.settings?.let { siteSettingsDao().delete(it) }
- site.lastResult?.let { validationResultsDao().delete(it) }
- site.retryPolicy?.let { retryPolicyDao().delete(it) }
- if (site.headers.any { it.id == 0L }) {
- throw IllegalStateException("Cannot delete header with ID = 0.")
- }
- headerDao().delete(site.headers)
- siteDao().delete(site)
-}
diff --git a/data/src/main/java/com/afollestad/nocknock/data/DatabaseMigrations.kt b/data/src/main/java/com/afollestad/nocknock/data/DatabaseMigrations.kt
deleted file mode 100644
index 0b158b8..0000000
--- a/data/src/main/java/com/afollestad/nocknock/data/DatabaseMigrations.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.data
-
-import androidx.room.migration.Migration
-import androidx.sqlite.db.SupportSQLiteDatabase
-
-/**
- * Migrates the database from version 1 to 2.
- *
- * @author Aidan Follestad (@afollestad)
- */
-class Database1to2Migration : Migration(1, 2) {
-
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL(
- "CREATE TABLE IF NOT EXISTS `retry_policies` (siteId INTEGER PRIMARY KEY NOT NULL, count INTEGER NOT NULL, minutes INTEGER NOT NULL, lastTryTimestamp INTEGER NOT NULL, triesLeft INTEGER NOT NULL)"
- )
- }
-}
-
-/**
- * Migrates the database from version 2 to 3.
- *
- * @author Aidan Follestad (@afollestad)
- */
-class Database2to3Migration : Migration(2, 3) {
-
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("ALTER TABLE `sites` ADD COLUMN tags TEXT NOT NULL DEFAULT ''")
- }
-}
-
-/**
- * Migrates the database from version 3 to 4.
- *
- * @author Aidan Follestad (@afollestad)
- */
-class Database3to4Migration : Migration(3, 4) {
-
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL(
- "CREATE TABLE IF NOT EXISTS `headers` (id INTEGER PRIMARY KEY NOT NULL, siteId INTEGER NOT NULL, `key` TEXT NOT NULL, value TEXT NOT NULL)"
- )
- }
-}
-
-/**
- * Migrates the database from version 4 to 5.
- *
- * @author Aidan Follestad (@afollestad)
- */
-class Database4to5Migration : Migration(4, 5) {
-
- override fun migrate(database: SupportSQLiteDatabase) {
- database.execSQL("ALTER TABLE `site_settings` ADD COLUMN certificate TEXT")
- }
-}
diff --git a/data/src/main/java/com/afollestad/nocknock/data/HeaderDao.kt b/data/src/main/java/com/afollestad/nocknock/data/HeaderDao.kt
deleted file mode 100644
index c34f0cb..0000000
--- a/data/src/main/java/com/afollestad/nocknock/data/HeaderDao.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.data
-
-import androidx.room.Dao
-import androidx.room.Delete
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy.FAIL
-import androidx.room.Query
-import androidx.room.Update
-import com.afollestad.nocknock.data.model.Header
-
-/** @author Aidan Follestad (@afollestad) */
-@Dao
-interface HeaderDao {
-
- @Query("SELECT * FROM headers ORDER BY siteId ASC")
- fun all(): List
-
- @Query("SELECT * FROM headers WHERE siteId = :siteId")
- fun forSite(siteId: Long): List
-
- @Insert(onConflict = FAIL)
- fun insert(headers: Header): Long
-
- @Insert(onConflict = FAIL)
- fun insert(headers: List): List
-
- @Update(onConflict = FAIL)
- fun update(header: Header): Int
-
- @Delete
- fun delete(headers: List): Int
-}
diff --git a/data/src/main/java/com/afollestad/nocknock/data/RetryPolicyDao.kt b/data/src/main/java/com/afollestad/nocknock/data/RetryPolicyDao.kt
deleted file mode 100644
index e130fed..0000000
--- a/data/src/main/java/com/afollestad/nocknock/data/RetryPolicyDao.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.data
-
-import androidx.room.Dao
-import androidx.room.Delete
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy.FAIL
-import androidx.room.Query
-import androidx.room.Update
-import com.afollestad.nocknock.data.model.RetryPolicy
-
-/** @author Aidan Follestad (@afollestad) */
-@Dao
-interface RetryPolicyDao {
-
- @Query("SELECT * FROM retry_policies ORDER BY siteId ASC")
- fun all(): List
-
- @Query("SELECT * FROM retry_policies WHERE siteId = :siteId LIMIT 1")
- fun forSite(siteId: Long): List
-
- @Insert(onConflict = FAIL)
- fun insert(policy: RetryPolicy): Long
-
- @Update(onConflict = FAIL)
- fun update(policy: RetryPolicy): Int
-
- @Delete
- fun delete(policy: RetryPolicy): Int
-}
diff --git a/data/src/main/java/com/afollestad/nocknock/data/SiteDao.kt b/data/src/main/java/com/afollestad/nocknock/data/SiteDao.kt
deleted file mode 100644
index 135a8f1..0000000
--- a/data/src/main/java/com/afollestad/nocknock/data/SiteDao.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.data
-
-import androidx.room.Dao
-import androidx.room.Delete
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy.FAIL
-import androidx.room.Query
-import androidx.room.Update
-import com.afollestad.nocknock.data.model.Site
-
-/** @author Aidan Follestad (@afollestad) */
-@Dao
-interface SiteDao {
-
- @Query("SELECT * FROM sites ORDER BY name ASC")
- fun all(): List
-
- @Query("SELECT * FROM sites WHERE id = :id LIMIT 1")
- fun one(id: Long): List
-
- @Insert(onConflict = FAIL)
- fun insert(site: Site): Long
-
- @Update(onConflict = FAIL)
- fun update(site: Site): Int
-
- @Delete
- fun delete(site: Site): Int
-}
diff --git a/data/src/main/java/com/afollestad/nocknock/data/SiteSettingsDao.kt b/data/src/main/java/com/afollestad/nocknock/data/SiteSettingsDao.kt
deleted file mode 100644
index b11f418..0000000
--- a/data/src/main/java/com/afollestad/nocknock/data/SiteSettingsDao.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.data
-
-import androidx.room.Dao
-import androidx.room.Delete
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy.FAIL
-import androidx.room.Query
-import androidx.room.Update
-import com.afollestad.nocknock.data.model.SiteSettings
-
-/** @author Aidan Follestad (@afollestad) */
-@Dao
-interface SiteSettingsDao {
-
- @Query("SELECT * FROM site_settings ORDER BY siteId ASC")
- fun all(): List
-
- @Query("SELECT * FROM site_settings WHERE siteId = :siteId LIMIT 1")
- fun forSite(siteId: Long): List
-
- @Insert(onConflict = FAIL)
- fun insert(siteSetting: SiteSettings): Long
-
- @Update(onConflict = FAIL)
- fun update(siteSetting: SiteSettings): Int
-
- @Delete
- fun delete(siteSetting: SiteSettings): Int
-}
diff --git a/data/src/main/java/com/afollestad/nocknock/data/ValidationResultsDao.kt b/data/src/main/java/com/afollestad/nocknock/data/ValidationResultsDao.kt
deleted file mode 100644
index d6c81dc..0000000
--- a/data/src/main/java/com/afollestad/nocknock/data/ValidationResultsDao.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.data
-
-import androidx.room.Dao
-import androidx.room.Delete
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy.FAIL
-import androidx.room.Query
-import androidx.room.Update
-import com.afollestad.nocknock.data.model.ValidationResult
-
-/** @author Aidan Follestad (@afollestad) */
-@Dao
-interface ValidationResultsDao {
-
- @Query("SELECT * FROM validation_results ORDER BY siteId ASC")
- fun all(): List
-
- @Query("SELECT * FROM validation_results WHERE siteId = :siteId LIMIT 1")
- fun forSite(siteId: Long): List
-
- @Insert(onConflict = FAIL)
- fun insert(siteSetting: ValidationResult): Long
-
- @Update(onConflict = FAIL)
- fun update(siteSetting: ValidationResult): Int
-
- @Delete
- fun delete(siteSetting: ValidationResult): Int
-}
diff --git a/data/src/main/java/com/afollestad/nocknock/data/model/Converters.kt b/data/src/main/java/com/afollestad/nocknock/data/model/Converters.kt
deleted file mode 100644
index b532829..0000000
--- a/data/src/main/java/com/afollestad/nocknock/data/model/Converters.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.data.model
-
-import androidx.room.TypeConverter
-
-/** @author Aidan Follestad (@afollestad) */
-class Converters {
-
- @TypeConverter
- fun fromStatus(status: Status): Int {
- return status.value
- }
-
- @TypeConverter
- fun toStatus(raw: Int): Status {
- return Status.fromValue(raw)
- }
-
- @TypeConverter
- fun fromValidationMode(mode: ValidationMode): Int {
- return mode.value
- }
-
- @TypeConverter
- fun toValidationMode(raw: Int): ValidationMode {
- return ValidationMode.fromValue(raw)
- }
-}
diff --git a/data/src/main/java/com/afollestad/nocknock/data/model/Header.kt b/data/src/main/java/com/afollestad/nocknock/data/model/Header.kt
deleted file mode 100644
index 9460af0..0000000
--- a/data/src/main/java/com/afollestad/nocknock/data/model/Header.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-@file:Suppress("unused")
-
-package com.afollestad.nocknock.data.model
-
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import java.io.Serializable
-
-/**
- * Represents an HTTP header that is sent with a site's validation attempts.
- *
- * @author Aidan Follestad (@afollestad)
- */
-@Entity(tableName = "headers")
-data class Header(
- /** The header's unique datrabase ID. */
- @PrimaryKey(autoGenerate = true) var id: Long = 0,
- /** The [Site] this header belong to. */
- var siteId: Long = 0,
- /** The header key/name. */
- var key: String = "",
- /** The header value. */
- var value: String = ""
-) : Serializable {
-
- constructor() : this(0, 0, "", "")
-}
diff --git a/data/src/main/java/com/afollestad/nocknock/data/model/RetryPolicy.kt b/data/src/main/java/com/afollestad/nocknock/data/model/RetryPolicy.kt
deleted file mode 100644
index a5c7831..0000000
--- a/data/src/main/java/com/afollestad/nocknock/data/model/RetryPolicy.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-@file:Suppress("unused")
-
-package com.afollestad.nocknock.data.model
-
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import com.afollestad.nocknock.utilities.ext.MINUTE
-import java.io.Serializable
-
-/**
- * Represents a site's retry policy, or how many times we
- * retry in a certain timespan before considering a site to
- * have a problem.
- *
- * @author Aidan Follestad (@afollestad)
- */
-@Entity(tableName = "retry_policies")
-data class RetryPolicy(
- /** The [Site] these settings belong to. */
- @PrimaryKey(autoGenerate = false) var siteId: Long = 0,
- /** How many times we want to retry. */
- var count: Int = 0,
- /**
- * In what amount of time (in minutes) we want
- * to perform those retries.
- */
- var minutes: Int = 0,
- /** The timestamp in milliseconds of the last attempt. */
- var lastTryTimestamp: Long = 0,
- /** How many retries we have left before considering the site to have problem. */
- var triesLeft: Int = -1
-) : Serializable {
-
- constructor() : this(0, 0, 0)
-
- // Say we are trying 6 times in 3 minutes, that means times per minute = 2.
- // Twice per minute means every 30 seconds.
- // 30 seconds = 30 * 1000 or 30,000 milliseconds.
- // 60,000 / 2 = 30,000.
- fun interval(): Long {
- if (count == 0 || minutes == 0) {
- return -1
- }
- val timesPerMinute = count.toFloat() / minutes.toFloat()
- return MINUTE / timesPerMinute.toSafeInt()
- }
-
- private fun Float.toSafeInt(): Int {
- val intValue = toInt()
- if (intValue == 0) {
- return 1
- }
- return intValue
- }
-}
diff --git a/data/src/main/java/com/afollestad/nocknock/data/model/Site.kt b/data/src/main/java/com/afollestad/nocknock/data/model/Site.kt
deleted file mode 100644
index 98feedc..0000000
--- a/data/src/main/java/com/afollestad/nocknock/data/model/Site.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.data.model
-
-import androidx.room.Entity
-import androidx.room.Ignore
-import androidx.room.PrimaryKey
-import com.afollestad.nocknock.data.model.Status.WAITING
-import com.afollestad.nocknock.utilities.ext.timeString
-import com.afollestad.nocknock.utilities.providers.CanNotifyModel
-import java.lang.System.currentTimeMillis
-import kotlin.math.max
-
-/** @author Aidan Follestad (@afollestad) */
-@Entity(tableName = "sites")
-data class Site(
- /** The site's unique ID. */
- @PrimaryKey(autoGenerate = true) var id: Long = 0,
- /** The site's user-given name. */
- var name: String,
- /** The URL at which validation attempts are made to. */
- var url: String,
- /** Comma separated tags for this site. */
- var tags: String,
- /** Settings for the site. */
- @Ignore var settings: SiteSettings?,
- /** The last validation attempt result for the site, if any. */
- @Ignore var lastResult: ValidationResult?,
- /** The site's retry policy, if any. */
- @Ignore var retryPolicy: RetryPolicy?,
- /** Request headers sent with this site's validation attempts. */
- @Ignore var headers: List
-) : CanNotifyModel {
-
- constructor() : this(0, "", "", "", null, null, null, emptyList())
-
- override fun notifyId(): Int = id.toInt()
-
- override fun notifyName(): String = name
-
- override fun notifyTag(): String = url
-
- override fun notifyDescription() = lastResult?.reason
-
- fun intervalText(): String {
- requireNotNull(settings) { "Settings not queried." }
- val lastCheck = lastResult?.timestampMs ?: -1
- val checkInterval = settings!!.validationIntervalMs
- val now = System.currentTimeMillis()
- val nextCheck = max(lastCheck, 0) + checkInterval
- return (nextCheck - now).timeString()
- }
-
- fun withStatus(
- status: Status? = null,
- reason: String? = null,
- timestamp: Long? = null
- ): Site {
- val newLastResult = lastResult?.copy(
- status = status ?: lastResult!!.status,
- reason = reason,
- timestampMs = timestamp ?: lastResult!!.timestampMs
- ) ?: ValidationResult(
- siteId = this.id,
- timestampMs = timestamp ?: currentTimeMillis(),
- status = status ?: WAITING,
- reason = reason
- )
- return this.copy(lastResult = newLastResult)
- }
-}
diff --git a/data/src/main/java/com/afollestad/nocknock/data/model/SiteSettings.kt b/data/src/main/java/com/afollestad/nocknock/data/model/SiteSettings.kt
deleted file mode 100644
index bc73589..0000000
--- a/data/src/main/java/com/afollestad/nocknock/data/model/SiteSettings.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-@file:Suppress("unused")
-
-package com.afollestad.nocknock.data.model
-
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
-import java.io.Serializable
-
-/**
- * Represents the current user configuration for a [Site].
- *
- * @author Aidan Follestad (@afollestad)
- */
-@Entity(tableName = "site_settings")
-data class SiteSettings(
- /** The [Site] these settings belong to. */
- @PrimaryKey(autoGenerate = false) var siteId: Long = 0,
- /** How often a validation attempt is made, in milliseconds. */
- var validationIntervalMs: Long,
- /** The method of which is used to validate the [Site]. */
- var validationMode: ValidationMode,
- /** Args that are used for the [ValidationMode], e.g. a search term. */
- var validationArgs: String?,
- /** Whether or not the [Site] is enabled for automatic periodic checks. */
- var disabled: Boolean,
- /** The network response timeout for validation attempts. */
- var networkTimeout: Int,
- /** The Uri to a self signed certificate. */
- var certificate: String?
-) : Serializable {
-
- constructor() : this(0, 0, STATUS_CODE, null, false, 0, null)
-}
diff --git a/data/src/main/java/com/afollestad/nocknock/data/model/Status.kt b/data/src/main/java/com/afollestad/nocknock/data/model/Status.kt
deleted file mode 100644
index e6294ea..0000000
--- a/data/src/main/java/com/afollestad/nocknock/data/model/Status.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.data.model
-
-import com.afollestad.nocknock.data.R.string
-import com.afollestad.nocknock.data.model.Status.CHECKING
-import com.afollestad.nocknock.data.model.Status.OK
-import com.afollestad.nocknock.data.model.Status.WAITING
-
-/**
- * Represents the current status of a [Site] - or whether or not the
- * site passed its most recent check.
- *
- * @author Aidan Follestad (@afollestad)
- */
-enum class Status(val value: Int) {
- /** The site has not been validated yet, pending the background job. */
- WAITING(1),
- /** The site is currently being validated. */
- CHECKING(2),
- /** The most recent validation attempt passed. */
- OK(3),
- /** The site did not pass a recent validation attempt. */
- 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 status: $value")
- }
- }
-}
-
-fun Status.textRes() = when (this) {
- OK -> string.everything_checks_out
- WAITING -> string.waiting
- CHECKING -> string.checking_status
- else -> 0
-}
-
-fun Status?.isPending() = this == WAITING || this == CHECKING
diff --git a/data/src/main/java/com/afollestad/nocknock/data/model/ValidationMode.kt b/data/src/main/java/com/afollestad/nocknock/data/model/ValidationMode.kt
deleted file mode 100644
index 31ddb04..0000000
--- a/data/src/main/java/com/afollestad/nocknock/data/model/ValidationMode.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.data.model
-
-/**
- * Represents the validation mode of a [Site] - this is the type of
- * check that is performed to get the site's current [Status].
- *
- * @author Aidan Follestad (@afollestad)
- */
-enum class ValidationMode(val value: Int) {
- /** The site is running normally if its status code is successful. */
- STATUS_CODE(1),
- /** The site is running normally if a piece of text is found in its response body. */
- TERM_SEARCH(2),
- /** The site is running normally if a block of given JavaScript executes successfully. */
- JAVASCRIPT(3);
-
- fun toIndex() = when (this) {
- STATUS_CODE -> 0
- TERM_SEARCH -> 1
- JAVASCRIPT -> 2
- }
-
- 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 fromIndex(index: Int) = when (index) {
- 0 -> STATUS_CODE
- 1 -> TERM_SEARCH
- 2 -> JAVASCRIPT
- else -> throw IllegalArgumentException("Index out of range: $index")
- }
- }
-}
diff --git a/data/src/main/java/com/afollestad/nocknock/data/model/ValidationResult.kt b/data/src/main/java/com/afollestad/nocknock/data/model/ValidationResult.kt
deleted file mode 100644
index 3b91214..0000000
--- a/data/src/main/java/com/afollestad/nocknock/data/model/ValidationResult.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-@file:Suppress("unused")
-
-package com.afollestad.nocknock.data.model
-
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import com.afollestad.nocknock.data.model.Status.OK
-import java.io.Serializable
-
-/**
- * Represents the most recent validation result for a [Site].
- *
- * @author Aidan Follestad (@afollestad)
- */
-@Entity(tableName = "validation_results")
-data class ValidationResult(
- /** The [Site] that this result belongs to. */
- @PrimaryKey(autoGenerate = false) var siteId: Long = 0,
- /** The timestamp in milliseconds at which this attempt was made. */
- var timestampMs: Long,
- /** The result of this validation attempt. */
- var status: Status,
- /** If the attempt was not successful, why it was not successful. */
- var reason: String?
-) : Serializable {
-
- constructor(): this(0, 0, OK, null)
-}
diff --git a/data/src/main/res/values/strings.xml b/data/src/main/res/values/strings.xml
deleted file mode 100644
index 09c580b..0000000
--- a/data/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
- Everything checks out!
- Checking status…
- Waiting…
-
-
diff --git a/dependencies.gradle b/dependencies.gradle
deleted file mode 100644
index e0e2412..0000000
--- a/dependencies.gradle
+++ /dev/null
@@ -1,58 +0,0 @@
-ext.versions = [
- // Project
- minSdk : 21,
- compileSdk : 28,
- buildTools : '28.0.3',
- publishVersion : '0.8.8',
- publishVersionCode : 46,
-
- // Plugins
- gradlePlugin : '3.4.0',
- spotlessPlugin : '3.22.0',
- versionPlugin : '0.21.0',
- googleServices : '4.2.0',
- fabricPlugin : '1.+',
-
- // Misc
- okHttp : '3.14.1',
- rhino : '1.7.10',
-
- // Kotlin
- kotlin : '1.3.30',
- coroutines : '1.2.0',
- koin : '1.0.2',
-
- // Google/AndroidX
- androidxAnnotations : '1.0.2',
- androidxCore : '1.0.2',
- androidxRecyclerView: '1.0.0',
- androidxBrowser : '1.0.0',
- googleMaterial : '1.0.0',
- room : '2.0.0',
- lifecycle : '2.0.0',
- firebaseCore : '16.0.8',
-
- // Rx
- rxJava : '2.2.8',
- rxBinding : '3.0.0-alpha1',
-
- // afollestad
- materialDialogs : '2.8.1',
- rxkPrefs : '1.2.5',
- vvalidator : '0.4.1',
-
- // Debugging
- timber : '4.7.1',
- fabric : '2.9.9@aar',
-
- // Unit testing
- junit : '4.12',
- mockito : '2.27.0',
- mockitoKotlin : '2.1.0',
- truth : '0.44',
-
- // UI testing
- androidxTestRunner : '1.1.1',
- androidxTest : '1.1.0',
- archTesting : '2.0.1'
-]
diff --git a/engine/.gitignore b/engine/.gitignore
deleted file mode 100644
index 796b96d..0000000
--- a/engine/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
diff --git a/engine/build.gradle b/engine/build.gradle
deleted file mode 100644
index 1808f4c..0000000
--- a/engine/build.gradle
+++ /dev/null
@@ -1,37 +0,0 @@
-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(':common')
- 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
-
- implementation 'org.koin:koin-android:' + versions.koin
-
- api 'com.squareup.okhttp3:okhttp:' + versions.okHttp
-
- implementation 'com.jakewharton.timber:timber:' + versions.timber
-
- 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'
\ No newline at end of file
diff --git a/engine/src/main/AndroidManifest.xml b/engine/src/main/AndroidManifest.xml
deleted file mode 100644
index 198c4f6..0000000
--- a/engine/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/EngineModule.kt b/engine/src/main/java/com/afollestad/nocknock/engine/EngineModule.kt
deleted file mode 100644
index 071960c..0000000
--- a/engine/src/main/java/com/afollestad/nocknock/engine/EngineModule.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.engine
-
-import com.afollestad.nocknock.engine.ssl.RealSslManager
-import com.afollestad.nocknock.engine.ssl.SslManager
-import com.afollestad.nocknock.engine.validation.RealValidationExecutor
-import com.afollestad.nocknock.engine.validation.ValidationExecutor
-import org.koin.dsl.module.module
-
-/** @author Aidan Follestad (@afollestad) */
-val engineModule = module {
-
- single {
- RealValidationExecutor(get(), get(), get(), get(), get(), get(), get())
- } bind ValidationExecutor::class
-
- factory { RealSslManager(get()) } bind SslManager::class
-}
diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/ssl/SslManager.kt b/engine/src/main/java/com/afollestad/nocknock/engine/ssl/SslManager.kt
deleted file mode 100644
index aceeb22..0000000
--- a/engine/src/main/java/com/afollestad/nocknock/engine/ssl/SslManager.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.engine.ssl
-
-import android.app.Application
-import android.content.Context
-import android.net.Uri
-import androidx.annotation.CheckResult
-import com.afollestad.nocknock.utilities.ext.toUri
-import okhttp3.OkHttpClient
-import java.io.BufferedInputStream
-import java.io.FileInputStream
-import java.security.KeyStore
-import java.security.cert.CertificateFactory
-import javax.net.ssl.SSLContext
-import javax.net.ssl.TrustManagerFactory
-import javax.net.ssl.X509TrustManager
-import timber.log.Timber.d as log
-
-/** @author Aidan Follestad (@afollestad) */
-interface SslManager {
-
- @CheckResult fun clientForCertificate(
- certUri: String,
- siteUri: String,
- client: OkHttpClient
- ): OkHttpClient
-}
-
-/** @author Aidan Follestad (@afollestad) **/
-class RealSslManager(
- private val app: Application
-) : SslManager {
-
- override fun clientForCertificate(
- certUri: String,
- siteUri: String,
- client: OkHttpClient
- ): OkHttpClient {
- val parsedCertUri = certUri.toUri()
- val parsedSiteUri = siteUri.toUri()
- val siteHost = parsedSiteUri.host ?: ""
-
- log("Loading certificate $certUri for host $siteHost")
- val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
- keyStore.load(null, null)
-
- val certInputStream = app.openUri(parsedCertUri)
- val bis = BufferedInputStream(certInputStream)
- val certificateFactory = CertificateFactory.getInstance("X.509")
-
- while (bis.available() > 0) {
- val cert = certificateFactory.generateCertificate(bis)
- keyStore.setCertificateEntry(siteHost, cert)
- }
-
- val trustManagerFactory =
- TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
- trustManagerFactory.init(keyStore)
-
- val trustManagers = trustManagerFactory.trustManagers
- val sslContext = SSLContext.getInstance("TLS")
- sslContext.init(null, trustManagers, null)
-
- val trustManager = trustManagers.first() as X509TrustManager
- log("Loaded successfully!")
- return client.newBuilder()
- .sslSocketFactory(sslContext.socketFactory, trustManager)
- .hostnameVerifier { hostname, _ ->
- log("Verifying hostname $hostname")
- hostname == siteHost
- }
- .build()
- }
-}
-
-private fun Context.openUri(uri: Uri) = when (uri.scheme) {
- "content" -> {
- contentResolver.openInputStream(uri) ?: throw IllegalStateException(
- "Unable to open input stream to $uri"
- )
- }
- "file" -> FileInputStream(uri.path)
- else -> FileInputStream(uri.toString())
-}
diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/validation/BootReceiver.kt b/engine/src/main/java/com/afollestad/nocknock/engine/validation/BootReceiver.kt
deleted file mode 100644
index cc80509..0000000
--- a/engine/src/main/java/com/afollestad/nocknock/engine/validation/BootReceiver.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.engine.validation
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.Intent.ACTION_BOOT_COMPLETED
-import com.afollestad.nocknock.utilities.Qualifiers.IO_DISPATCHER
-import com.afollestad.nocknock.utilities.Qualifiers.MAIN_DISPATCHER
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.koin.standalone.KoinComponent
-import org.koin.standalone.inject
-import timber.log.Timber.d as log
-
-/** @author Aidan Follestad (@afollestad) */
-class BootReceiver : BroadcastReceiver(), KoinComponent {
-
- private val validationManager by inject()
- private val mainDispatcher by inject(name = MAIN_DISPATCHER)
- private val ioDispatcher by inject(name = IO_DISPATCHER)
-
- override fun onReceive(
- context: Context,
- intent: Intent
- ) {
- require(ACTION_BOOT_COMPLETED == intent.action) {
- "BootReceiver should only receive ACTION_BOOT_COMPLETED intents."
- }
-
- log("Received boot event! Let's go.")
-
- val pendingResult = goAsync()
- GlobalScope.launch(mainDispatcher) {
- withContext(ioDispatcher) { validationManager.ensureScheduledValidations() }
- pendingResult.resultCode = 0
- pendingResult.finish()
- }
- }
-}
diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/validation/ValidationExecutor.kt b/engine/src/main/java/com/afollestad/nocknock/engine/validation/ValidationExecutor.kt
deleted file mode 100644
index d47d616..0000000
--- a/engine/src/main/java/com/afollestad/nocknock/engine/validation/ValidationExecutor.kt
+++ /dev/null
@@ -1,225 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.engine.validation
-
-import android.app.job.JobScheduler
-import android.app.job.JobScheduler.RESULT_SUCCESS
-import android.net.Uri
-import com.afollestad.nocknock.data.AppDatabase
-import com.afollestad.nocknock.data.allSites
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.data.model.Status.ERROR
-import com.afollestad.nocknock.data.model.Status.OK
-import com.afollestad.nocknock.engine.R
-import com.afollestad.nocknock.engine.ssl.SslManager
-import com.afollestad.nocknock.engine.validation.ValidationJob.Companion.KEY_SITE_ID
-import com.afollestad.nocknock.utilities.ext.isNotNullOrEmpty
-import com.afollestad.nocknock.utilities.providers.BundleProvider
-import com.afollestad.nocknock.utilities.providers.JobInfoProvider
-import com.afollestad.nocknock.utilities.providers.StringProvider
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.Response
-import org.jetbrains.annotations.TestOnly
-import java.net.SocketTimeoutException
-import java.util.concurrent.TimeUnit.MILLISECONDS
-import kotlin.math.max
-import timber.log.Timber.d as log
-
-/** @author Aidan Follestad (@afollestad) */
-data class CheckResult(
- val model: Site,
- val response: Response? = null
-)
-
-typealias ClientTimeoutChanger = (client: OkHttpClient, timeout: Int) -> OkHttpClient
-
-typealias UriConverter = (String) -> Uri
-
-/** @author Aidan Follestad (@afollestad) */
-interface ValidationExecutor {
-
- suspend fun ensureScheduledValidations()
-
- fun scheduleValidation(
- site: Site,
- rightNow: Boolean = false,
- cancelPrevious: Boolean = rightNow,
- fromFinishingJob: Boolean = false,
- overrideDelay: Long = -1
- )
-
- fun cancelScheduledValidation(site: Site)
-
- suspend fun performValidation(site: Site): CheckResult
-}
-
-class RealValidationExecutor(
- private val jobScheduler: JobScheduler,
- private val okHttpClient: OkHttpClient,
- private val stringProvider: StringProvider,
- private val bundleProvider: BundleProvider,
- private val jobInfoProvider: JobInfoProvider,
- private val database: AppDatabase,
- private val sslManager: SslManager
-) : ValidationExecutor {
-
- private var clientTimeoutChanger: ClientTimeoutChanger = { client, timeout ->
- client.newBuilder()
- .callTimeout(timeout.toLong(), MILLISECONDS)
- .build()
- }
-
- override suspend fun ensureScheduledValidations() {
- val sites = database.allSites()
- if (sites.isEmpty()) {
- return
- }
- log("Ensuring enabled sites have scheduled validations.")
- sites.filter { it.settings?.disabled != true }
- .forEach { site ->
- val existingJob = jobForSite(site)
- if (existingJob == null) {
- log("Site ${site.id} does NOT have a scheduled job, running one now.")
- scheduleValidation(site = site, rightNow = true)
- } else {
- log("Site ${site.id} already has a scheduled job. Nothing to do.")
- }
- }
- }
-
- override fun scheduleValidation(
- site: Site,
- rightNow: Boolean,
- cancelPrevious: Boolean,
- fromFinishingJob: Boolean,
- overrideDelay: Long
- ) {
- check(site.id != 0L) { "Cannot schedule validations for jobs with no ID." }
- val siteSettings = site.settings
- requireNotNull(siteSettings) { "Site settings must be populated." }
-
- if (cancelPrevious) {
- cancelScheduledValidation(site)
- } else if (!fromFinishingJob) {
- val existingJob = jobForSite(site)
- check(existingJob == null) {
- "Site ${site.id} already has a scheduled job, and cancelPrevious = false."
- }
- }
-
- log("Requesting a validation job for site to be scheduled: $site")
- val extras = bundleProvider.createPersistable {
- putLong(KEY_SITE_ID, site.id)
- }
- val jobInfo = jobInfoProvider.createCheckJob(
- id = site.id.toInt(),
- onlyUnmeteredNetwork = false,
- delayMs = when {
- rightNow -> 1
- overrideDelay > -1 -> overrideDelay
- else -> siteSettings.validationIntervalMs
- },
- extras = extras,
- target = ValidationJob::class.java
- )
-
- val dispatchResult = jobScheduler.schedule(jobInfo)
- if (dispatchResult != RESULT_SUCCESS) {
- log("Failed to schedule a validation job for site: ${site.id}")
- } else {
- log("Validation job successfully scheduled for site: ${site.id}")
- }
- }
-
- override fun cancelScheduledValidation(site: Site) {
- check(site.id != 0L) { "Cannot cancel scheduled validations for jobs with no ID." }
- log("Cancelling scheduled validations for site: ${site.id}")
- jobScheduler.cancel(site.id.toInt())
- }
-
- override suspend fun performValidation(site: Site): CheckResult {
- check(site.id != 0L) { "Cannot schedule validations for jobs with no ID." }
- val siteSettings = site.settings
- requireNotNull(siteSettings) { "Site settings must be populated." }
- log("performValidation(${site.id}) - GET ${site.url}")
-
- val request = Request.Builder()
- .apply {
- url(site.url)
- get()
- site.headers
- .filter { header -> header.key.isNotNullOrEmpty() }
- .forEach { header ->
- addHeader(header.key, header.value)
- }
- }
- .build()
-
- return try {
- val timeout = max(siteSettings.networkTimeout, 1)
- val clientWithTimeout = clientTimeoutChanger(okHttpClient, timeout)
- val client = if (siteSettings.certificate.isNotNullOrEmpty()) {
- sslManager.clientForCertificate(
- certUri = siteSettings.certificate!!,
- siteUri = site.url,
- client = clientWithTimeout
- )
- } else {
- clientWithTimeout
- }
- val response = client.newCall(request)
- .execute()
-
- if (response.isSuccessful) {
- log("performValidation(${site.id}) = Successful")
- CheckResult(
- model = site.withStatus(status = OK, reason = null),
- response = response
- )
- } else {
- log("performValidation(${site.id}) = Failure, HTTP code ${response.code()}")
- CheckResult(
- model = site.withStatus(
- status = ERROR,
- reason = "Response ${response.code()} - ${response.body()?.string() ?: "Unknown"}"
- ),
- response = response
- )
- }
- } catch (timeoutEx: SocketTimeoutException) {
- log("performValidation(${site.id}) = Socket Timeout")
- CheckResult(
- model = site.withStatus(
- status = ERROR,
- reason = stringProvider.get(R.string.timeout)
- )
- )
- } catch (ex: Exception) {
- ex.printStackTrace()
- log("performValidation(${site.id}) = Error: ${ex.message}")
- CheckResult(model = site.withStatus(status = ERROR, reason = ex.message))
- }
- }
-
- private fun jobForSite(site: Site) =
- jobScheduler.allPendingJobs
- .firstOrNull { job -> job.id == site.id.toInt() }
-
- @TestOnly fun setClientTimeoutChanger(changer: ClientTimeoutChanger) {
- this.clientTimeoutChanger = changer
- }
-}
diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/validation/ValidationJob.kt b/engine/src/main/java/com/afollestad/nocknock/engine/validation/ValidationJob.kt
deleted file mode 100644
index 435ff89..0000000
--- a/engine/src/main/java/com/afollestad/nocknock/engine/validation/ValidationJob.kt
+++ /dev/null
@@ -1,242 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.engine.validation
-
-import android.app.job.JobParameters
-import android.app.job.JobService
-import android.content.Intent
-import com.afollestad.nocknock.data.AppDatabase
-import com.afollestad.nocknock.data.getSite
-import com.afollestad.nocknock.data.model.RetryPolicy
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.data.model.Status
-import com.afollestad.nocknock.data.model.Status.CHECKING
-import com.afollestad.nocknock.data.model.Status.ERROR
-import com.afollestad.nocknock.data.model.Status.OK
-import com.afollestad.nocknock.data.model.Status.WAITING
-import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
-import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
-import com.afollestad.nocknock.data.model.isPending
-import com.afollestad.nocknock.data.updateSite
-import com.afollestad.nocknock.engine.BuildConfig.APPLICATION_ID
-import com.afollestad.nocknock.notifications.NockNotificationManager
-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 org.koin.android.ext.android.inject
-import java.lang.System.currentTimeMillis
-import timber.log.Timber.d as log
-
-/**
- * The job which is sent to the system JobScheduler to perform site validation in the background.
- *
- * @author Aidan Follestad (@afollestad)
- */
-class ValidationJob : 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 val database by inject()
- private val validationManager by inject()
- private val notificationManager by inject()
-
- override fun onStartJob(params: JobParameters): Boolean {
- val siteId = params.extras.getLong(KEY_SITE_ID)
-
- GlobalScope.launch(Main) {
- val site = withContext(IO) { database.getSite(siteId) }
- if (site == null) {
- log("Unable to find a site for ID $siteId, this job will not be rescheduled.")
- return@launch jobFinished(params, false)
- }
-
- val siteSettings = site.settings
- requireNotNull(siteSettings) { "Site settings must be populated." }
-
- 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 lastResult = site.lastResult
- if (lastResult != null) {
- log("Result of previous attempt: ${lastResult.status}")
- }
-
- val jobResult = async(IO) {
- updateStatus(site, CHECKING)
- val checkResult = validationManager.performValidation(site)
- val resultModel = checkResult.model
- val resultResponse = checkResult.response
- val result = resultModel.lastResult!!
-
- if (result.status != OK) {
- log("Got unsuccessful check status back: ${result.reason}")
- return@async updateStatus(site = resultModel)
- } else {
- when (siteSettings.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(siteSettings.validationArgs ?: "")) {
- updateStatus(
- resultModel.withStatus(
- status = ERROR,
- reason = "Term \"${siteSettings.validationArgs}\" not found in response body."
- )
- )
- } else {
- updateStatus(site = resultModel)
- }
- }
- JAVASCRIPT -> {
- val body = resultResponse?.body()?.string() ?: ""
- log("Using JAVASCRIPT validation mode on body of length: ${body.length}")
- val reason = JavaScript.eval(siteSettings.validationArgs ?: "", body)
- return@async if (reason != null) {
- updateStatus(resultModel.withStatus(reason = reason), status = ERROR)
- } else {
- resultModel
- }
- }
- STATUS_CODE -> {
- // We already know the status code is successful because we are in this else branch
- log("Using STATUS_CODE validation, which has passed!")
- updateStatus(
- resultModel.withStatus(
- status = OK,
- reason = null
- )
- )
- }
- else -> {
- throw IllegalArgumentException(
- "Unknown validation mode: ${siteSettings.validationArgs}"
- )
- }
- }
- }
- }.await()
-
- if (jobResult.lastResult!!.status == OK) {
- notificationManager.cancelStatusNotification(jobResult)
- if (lastResult != null && lastResult.status == ERROR) {
- notificationManager.postValidationSuccessNotification(jobResult)
- }
- } else {
- val retryPolicy = site.retryPolicy
- if (retryPolicy != null) {
- log("Check for site ${site.id} was unsuccessful. BUT we have a retryPolicy.")
-
- if (retryPolicy.triesLeft == -1 || retryPolicy.triesLeft > 0) {
- if (retryPolicy.triesLeft == -1) {
- retryPolicy.triesLeft = retryPolicy.count
- } else {
- retryPolicy.triesLeft -= 1
- }
- updateTriesLeft(retryPolicy, retryPolicy.triesLeft)
-
- val interval = retryPolicy.interval()
- validationManager.scheduleValidation(
- site = jobResult,
- fromFinishingJob = true,
- overrideDelay = interval
- )
- log("Scheduling retry in $interval milliseconds.")
-
- return@launch
- } else {
- updateTriesLeft(retryPolicy, -1)
- log("No tries left, continuing to error notification.")
- }
- }
-
- notificationManager.postValidationErrorNotification(jobResult)
- }
-
- validationManager.scheduleValidation(
- site = jobResult,
- fromFinishingJob = true
- )
- }
-
- return true
- }
-
- override fun onStopJob(params: JobParameters): Boolean {
- val siteId = params.extras.getLong(KEY_SITE_ID)
- log("Check job for site $siteId is done")
- return true
- }
-
- private suspend fun updateStatus(
- site: Site,
- status: Status = site.lastResult?.status ?: WAITING
- ): Site {
- log("Updating ${site.name} (${site.url}) status to $status...")
-
- val lastCheckTime =
- if (status.isPending()) site.lastResult?.timestampMs ?: -1
- else currentTimeMillis()
- val reason =
- if (status == OK) null
- else site.lastResult?.reason ?: "Unknown"
-
- if (site.retryPolicy != null && status == OK) {
- site.retryPolicy = site.retryPolicy!!.copy(
- triesLeft = -1,
- lastTryTimestamp = 0
- )
- }
-
- val updatedModel = site.withStatus(
- status = status,
- timestamp = lastCheckTime,
- reason = reason
- )
- database.updateSite(updatedModel)
-
- withContext(Main) {
- sendBroadcast(Intent(ACTION_STATUS_UPDATE).apply {
- putExtra(KEY_UPDATE_MODEL, updatedModel)
- })
- }
- return updatedModel
- }
-
- private suspend fun updateTriesLeft(
- retryPolicy: RetryPolicy,
- triesLeft: Int
- ) {
- retryPolicy.triesLeft = triesLeft
- retryPolicy.lastTryTimestamp = currentTimeMillis()
- withContext(IO) {
- database.retryPolicyDao()
- .update(retryPolicy)
- }
- log("Tries left for site ${retryPolicy.siteId}: $triesLeft")
- }
-}
diff --git a/engine/src/main/res/values/strings.xml b/engine/src/main/res/values/strings.xml
deleted file mode 100644
index d899d07..0000000
--- a/engine/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- Nock Nock Status Service
- Request timed out! Your server is probably down.
-
-
diff --git a/engine/src/test/java/com/afollestad/nocknock/engine/TestData.kt b/engine/src/test/java/com/afollestad/nocknock/engine/TestData.kt
deleted file mode 100644
index cfe4a56..0000000
--- a/engine/src/test/java/com/afollestad/nocknock/engine/TestData.kt
+++ /dev/null
@@ -1,189 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.engine
-
-import android.content.Intent
-import com.afollestad.nocknock.data.AppDatabase
-import com.afollestad.nocknock.data.HeaderDao
-import com.afollestad.nocknock.data.RetryPolicyDao
-import com.afollestad.nocknock.data.SiteDao
-import com.afollestad.nocknock.data.SiteSettingsDao
-import com.afollestad.nocknock.data.ValidationResultsDao
-import com.afollestad.nocknock.data.model.Header
-import com.afollestad.nocknock.data.model.RetryPolicy
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.data.model.SiteSettings
-import com.afollestad.nocknock.data.model.Status
-import com.afollestad.nocknock.data.model.Status.OK
-import com.afollestad.nocknock.data.model.ValidationMode
-import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.data.model.ValidationResult
-import com.nhaarman.mockitokotlin2.doAnswer
-import com.nhaarman.mockitokotlin2.doReturn
-import com.nhaarman.mockitokotlin2.isA
-import com.nhaarman.mockitokotlin2.mock
-import java.lang.System.currentTimeMillis
-
-fun fakeIntent(action: String): Intent {
- return mock {
- on { getAction() } doReturn action
- }
-}
-
-fun fakeSettingsModel(
- id: Long,
- validationMode: ValidationMode = STATUS_CODE
-) = SiteSettings(
- siteId = id,
- validationIntervalMs = 600000,
- validationMode = validationMode,
- validationArgs = null,
- disabled = false,
- networkTimeout = 10000,
- certificate = null
-)
-
-fun fakeResultModel(
- id: Long,
- status: Status = OK,
- reason: String? = null
-) = ValidationResult(
- siteId = id,
- status = status,
- reason = reason,
- timestampMs = currentTimeMillis()
-)
-
-fun fakeRetryPolicy(
- id: Long,
- count: Int = 3,
- minutes: Int = 6
-) = RetryPolicy(
- siteId = id,
- count = count,
- minutes = minutes
-)
-
-fun fakeHeaders(siteId: Long): List {
- return listOf(
- Header(id = siteId + 1, siteId = siteId, key = "Content-Type", value = "text/html"),
- Header(id = siteId + 2, siteId = siteId, key = "User-Agent", value = "NockNock")
- )
-}
-
-fun fakeModel(id: Long) = Site(
- id = id,
- name = "Test",
- url = "https://test.com",
- tags = "",
- settings = fakeSettingsModel(id),
- lastResult = fakeResultModel(id),
- retryPolicy = fakeRetryPolicy(id),
- headers = fakeHeaders(id)
-)
-
-val MOCK_MODEL_1 = fakeModel(1)
-val MOCK_MODEL_2 = fakeModel(2)
-val MOCK_MODEL_3 = fakeModel(3)
-val ALL_MOCK_MODELS = listOf(MOCK_MODEL_1, MOCK_MODEL_2, MOCK_MODEL_3)
-
-fun mockDatabase(): AppDatabase {
- val siteDao = mock {
- on { insert(isA()) } doReturn 1
- on { one(isA()) } doAnswer { inv ->
- val id = inv.getArgument(0)
- return@doAnswer when (id) {
- 1L -> listOf(MOCK_MODEL_1)
- 2L -> listOf(MOCK_MODEL_2)
- 3L -> listOf(MOCK_MODEL_3)
- else -> listOf()
- }
- }
- on { all() } doReturn ALL_MOCK_MODELS
- on { update(isA()) } doAnswer { inv ->
- return@doAnswer inv.arguments.size
- }
- on { delete(isA()) } doAnswer { inv ->
- return@doAnswer inv.arguments.size
- }
- }
- val settingsDao = mock {
- on { insert(isA()) } doReturn 1L
- on { forSite(isA()) } doAnswer { inv ->
- val id = inv.getArgument(0)
- return@doAnswer when (id) {
- 1L -> listOf(MOCK_MODEL_1.settings!!)
- 2L -> listOf(MOCK_MODEL_2.settings!!)
- 3L -> listOf(MOCK_MODEL_3.settings!!)
- else -> listOf()
- }
- }
- on { update(isA()) } doReturn 1
- on { delete(isA()) } doReturn 1
- }
- val resultsDao = mock {
- on { insert(isA()) } doReturn 1L
- on { forSite(isA()) } doAnswer { inv ->
- val id = inv.getArgument(0)
- return@doAnswer when (id) {
- 1L -> listOf(MOCK_MODEL_1.lastResult!!)
- 2L -> listOf(MOCK_MODEL_2.lastResult!!)
- 3L -> listOf(MOCK_MODEL_3.lastResult!!)
- else -> listOf()
- }
- }
- on { update(isA()) } doReturn 1
- on { delete(isA()) } doReturn 1
- }
- val retryDao = mock {
- on { insert(isA()) } doReturn 1L
- on { forSite(isA()) } doAnswer { inv ->
- val id = inv.getArgument(0)
- return@doAnswer when (id) {
- 1L -> listOf(MOCK_MODEL_1.retryPolicy!!)
- 2L -> listOf(MOCK_MODEL_2.retryPolicy!!)
- 3L -> listOf(MOCK_MODEL_3.retryPolicy!!)
- else -> listOf()
- }
- }
- on { update(isA()) } doReturn 1
- on { delete(isA()) } doReturn 1
- }
- val headerDao = mock {
- on { all() } doReturn MOCK_MODEL_1.headers + MOCK_MODEL_2.headers + MOCK_MODEL_3.headers
- on { forSite(isA()) } doAnswer { inv ->
- val id = inv.getArgument(0)
- return@doAnswer when (id) {
- 1L -> MOCK_MODEL_1.headers
- 2L -> MOCK_MODEL_2.headers
- 3L -> MOCK_MODEL_3.headers
- else -> listOf()
- }
- }
- on { insert(isA()) } doReturn 1L
- on { insert(isA>()) } doReturn listOf(1L, 2L)
- on { update(isA()) } doReturn 1
- on { delete(isA()) } doReturn 1
- }
-
- return mock {
- on { siteDao() } doReturn siteDao
- on { siteSettingsDao() } doReturn settingsDao
- on { validationResultsDao() } doReturn resultsDao
- on { retryPolicyDao() } doReturn retryDao
- on { headerDao() } doReturn headerDao
- }
-}
diff --git a/engine/src/test/java/com/afollestad/nocknock/engine/TestUtil.kt b/engine/src/test/java/com/afollestad/nocknock/engine/TestUtil.kt
deleted file mode 100644
index 7255e9f..0000000
--- a/engine/src/test/java/com/afollestad/nocknock/engine/TestUtil.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.engine
-
-import android.app.job.JobInfo
-import android.content.ComponentName
-import android.os.PersistableBundle
-import com.afollestad.nocknock.data.AppDatabase
-import com.afollestad.nocknock.data.model.Site
-import com.afollestad.nocknock.utilities.providers.BundleProvider
-import com.afollestad.nocknock.utilities.providers.IBundle
-import com.afollestad.nocknock.utilities.providers.IBundler
-import com.afollestad.nocknock.utilities.providers.JobInfoProvider
-import com.nhaarman.mockitokotlin2.any
-import com.nhaarman.mockitokotlin2.doAnswer
-import com.nhaarman.mockitokotlin2.doReturn
-import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.whenever
-
-fun testBundleProvider(): BundleProvider {
- val provider = mock()
- whenever(provider.createPersistable(any())).doAnswer {
- val realBundle = mock()
- val creator = it.getArgument(0)
- creator(object : IBundle {
- override fun putLong(
- key: String,
- value: Long
- ) {
- whenever(realBundle.getLong(key)).doReturn(value)
- }
- })
- return@doAnswer realBundle
- }
- return provider
-}
-
-fun testJobInfoProvider(): JobInfoProvider {
- val provider = mock()
- whenever(provider.createCheckJob(any(), any(), any(), any(), any())).doAnswer { inv ->
- val jobInfo = mock()
- val id = inv.getArgument(0)
- val delay = inv.getArgument(2)
- val extras = inv.getArgument(3)
- val target = inv.getArgument>(4)
- val component = mock()
- whenever(component.className).doReturn(target.name)
-
- whenever(jobInfo.id).doReturn(id)
- whenever(jobInfo.minLatencyMillis).doReturn(delay)
- whenever(jobInfo.extras).doReturn(extras)
- whenever(jobInfo.service).doReturn(component)
-
- return@doAnswer jobInfo
- }
- return provider
-}
-
-fun AppDatabase.setAllSites(vararg sites: Site) {
- whenever(siteDao().all()).doReturn(listOf(*sites))
- for (site in sites) {
- whenever(siteSettingsDao().forSite(site.id))
- .doReturn(listOf(site.settings!!))
- if (site.lastResult != null) {
- whenever(validationResultsDao().forSite(site.id))
- .doReturn(listOf(site.lastResult!!))
- }
- if (site.retryPolicy != null) {
- whenever(retryPolicyDao().forSite(site.id))
- .doReturn(listOf(site.retryPolicy!!))
- }
- whenever(headerDao().forSite(site.id))
- .doReturn(site.headers)
- }
-}
diff --git a/engine/src/test/java/com/afollestad/nocknock/engine/ValidationExecutorTest.kt b/engine/src/test/java/com/afollestad/nocknock/engine/ValidationExecutorTest.kt
deleted file mode 100644
index dff3217..0000000
--- a/engine/src/test/java/com/afollestad/nocknock/engine/ValidationExecutorTest.kt
+++ /dev/null
@@ -1,345 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.engine
-
-import android.app.job.JobInfo
-import android.app.job.JobScheduler
-import com.afollestad.nocknock.data.model.Header
-import com.afollestad.nocknock.data.model.Status.ERROR
-import com.afollestad.nocknock.data.model.Status.OK
-import com.afollestad.nocknock.engine.ssl.SslManager
-import com.afollestad.nocknock.engine.validation.RealValidationExecutor
-import com.afollestad.nocknock.engine.validation.ValidationJob.Companion.KEY_SITE_ID
-import com.afollestad.nocknock.utilities.providers.StringProvider
-import com.google.common.truth.Truth.assertThat
-import com.nhaarman.mockitokotlin2.any
-import com.nhaarman.mockitokotlin2.argumentCaptor
-import com.nhaarman.mockitokotlin2.doAnswer
-import com.nhaarman.mockitokotlin2.doReturn
-import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.never
-import com.nhaarman.mockitokotlin2.verify
-import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
-import com.nhaarman.mockitokotlin2.whenever
-import kotlinx.coroutines.runBlocking
-import okhttp3.Call
-import okhttp3.OkHttpClient
-import okhttp3.Protocol.HTTP_2
-import okhttp3.Request
-import okhttp3.Response
-import okhttp3.ResponseBody
-import org.junit.Test
-import java.net.SocketTimeoutException
-
-class ValidationExecutorTest {
-
- private val timeoutError = "Oh no, a timeout"
-
- private val jobScheduler = mock()
- private val okHttpClient = mock()
- private val stringProvider = mock {
- on { get(R.string.timeout) } doReturn timeoutError
- }
- private val bundleProvider = testBundleProvider()
- private val jobInfoProvider = testJobInfoProvider()
- private val database = mockDatabase()
- private val sslManager = mock {
- on { clientForCertificate(any(), any(), any()) } doAnswer { inv ->
- inv.getArgument(2)
- }
- }
-
- private val manager = RealValidationExecutor(
- jobScheduler,
- okHttpClient,
- stringProvider,
- bundleProvider,
- jobInfoProvider,
- database,
- sslManager
- ).apply {
- setClientTimeoutChanger { _, timeout ->
- whenever(okHttpClient.callTimeoutMillis()).doReturn(timeout)
- return@setClientTimeoutChanger okHttpClient
- }
- }
-
- @Test fun ensureScheduledValidations_noEnabledSites() = runBlocking {
- val model1 = fakeModel(id = 1)
- model1.settings = model1.settings!!.copy(disabled = true)
- database.setAllSites(model1)
-
- manager.ensureScheduledValidations()
-
- verifyNoMoreInteractions(jobScheduler)
- }
-
- @Test fun ensureScheduledValidations_sitesAlreadyHaveJobs() = runBlocking {
- val model1 = fakeModel(id = 1)
- val job1 = fakeJob(1)
- database.setAllSites(model1)
- whenever(jobScheduler.allPendingJobs).doReturn(listOf(job1))
-
- manager.ensureScheduledValidations()
-
- verify(jobScheduler, never()).schedule(any())
- }
-
- @Test fun ensureScheduledValidations() = runBlocking {
- val model1 = fakeModel(id = 1)
- database.setAllSites(model1)
-
- whenever(jobScheduler.allPendingJobs).doReturn(listOf())
-
- manager.ensureScheduledValidations()
-
- val jobCaptor = argumentCaptor()
- verify(jobScheduler).schedule(jobCaptor.capture())
- val jobInfo = jobCaptor.allValues.single()
- assertThat(jobInfo.id).isEqualTo(model1.id)
- assertThat(jobInfo.extras.getLong(KEY_SITE_ID)).isEqualTo(model1.id)
- }
-
- @Test fun scheduleValidation_rightNow() {
- val model1 = fakeModel(id = 1)
- whenever(jobScheduler.allPendingJobs).doReturn(listOf())
-
- manager.scheduleValidation(
- site = model1,
- rightNow = true
- )
-
- val jobCaptor = argumentCaptor()
- verify(jobScheduler).schedule(jobCaptor.capture())
- verify(jobScheduler).cancel(1)
-
- val jobInfo = jobCaptor.allValues.single()
- assertThat(jobInfo.id).isEqualTo(model1.id)
- assertThat(jobInfo.extras.getLong(KEY_SITE_ID)).isEqualTo(model1.id)
- }
-
- @Test(expected = IllegalStateException::class)
- fun scheduleValidation_notFromFinishingJob_haveExistingJob() {
- val model1 = fakeModel(id = 1)
- val job1 = fakeJob(1)
- whenever(jobScheduler.allPendingJobs).doReturn(listOf(job1))
-
- manager.scheduleValidation(
- site = model1,
- fromFinishingJob = false
- )
- }
-
- @Test fun scheduleValidation_fromFinishingJob_haveExistingJob() {
- val model1 = fakeModel(id = 1)
- val job1 = fakeJob(1)
- whenever(jobScheduler.allPendingJobs).doReturn(listOf(job1))
-
- manager.scheduleValidation(
- site = model1,
- fromFinishingJob = true
- )
-
- val jobCaptor = argumentCaptor()
- verify(jobScheduler).schedule(jobCaptor.capture())
- verify(jobScheduler, never()).cancel(any())
-
- val jobInfo = jobCaptor.allValues.single()
- assertThat(jobInfo.id).isEqualTo(model1.id)
- assertThat(jobInfo.extras.getLong(KEY_SITE_ID)).isEqualTo(model1.id)
- }
-
- @Test fun scheduleValidation() {
- val model1 = fakeModel(id = 1)
- whenever(jobScheduler.allPendingJobs).doReturn(listOf())
-
- manager.scheduleValidation(
- site = model1,
- fromFinishingJob = true
- )
-
- val jobCaptor = argumentCaptor()
- verify(jobScheduler).schedule(jobCaptor.capture())
- verify(jobScheduler, never()).cancel(any())
-
- val jobInfo = jobCaptor.allValues.single()
- assertThat(jobInfo.id).isEqualTo(model1.id)
- assertThat(jobInfo.extras.getLong(KEY_SITE_ID)).isEqualTo(model1.id)
- }
-
- @Test fun cancelScheduledValidation() {
- val model1 = fakeModel(id = 1)
- manager.cancelScheduledValidation(model1)
- verify(jobScheduler).cancel(1)
- }
-
- @Test fun performValidation_httpNotSuccess() = runBlocking {
- val response = fakeResponse(500, "Internal Server Error", "Hello World")
- val call = mock {
- on { execute() } doReturn response
- }
- whenever(okHttpClient.newCall(any())).doReturn(call)
-
- val model1 = fakeModel(id = 1)
- val result = manager.performValidation(model1)
-
- assertThat(result.model).isEqualTo(
- model1.copy(
- lastResult = model1.lastResult?.copy(
- status = ERROR,
- reason = "Response 500 - Hello World"
- )
- )
- )
- }
-
- @Test fun performValidation_socketTimeout() = runBlocking {
- val error = SocketTimeoutException("Oh no!")
- val call = mock {
- on { execute() } doAnswer { throw error }
- }
- whenever(okHttpClient.newCall(any())).doReturn(call)
-
- val model1 = fakeModel(id = 1)
- val result = manager.performValidation(model1)
-
- assertThat(result.model).isEqualTo(
- model1.copy(
- lastResult = model1.lastResult?.copy(
- status = ERROR,
- reason = timeoutError
- )
- )
- )
- }
-
- @Test fun performValidation_exception() = runBlocking {
- val error = Exception("Oh no!")
- val call = mock {
- on { execute() } doAnswer { throw error }
- }
- whenever(okHttpClient.newCall(any())).doReturn(call)
-
- val model1 = fakeModel(id = 1)
- val result = manager.performValidation(model1)
-
- assertThat(result.model).isEqualTo(
- model1.copy(
- lastResult = model1.lastResult?.copy(
- status = ERROR,
- reason = "Oh no!"
- )
- )
- )
- }
-
- @Test fun performValidation_success_withHeaders() = runBlocking {
- val requestCaptor = argumentCaptor()
- val response = fakeResponse(200, "OK", "Hello World")
-
- val call = mock {
- on { execute() } doReturn response
- }
- whenever(okHttpClient.newCall(requestCaptor.capture()))
- .doReturn(call)
-
- val model1 = fakeModel(id = 1).copy(
- headers = listOf(
- Header(
- key = "X-Test-Header",
- value = "Hello, World!"
- )
- )
- )
- val result = manager.performValidation(model1)
- val httpRequest = requestCaptor.firstValue
-
- assertThat(result.model).isEqualTo(
- model1.copy(
- lastResult = model1.lastResult?.copy(
- status = OK,
- reason = null
- )
- )
- )
- assertThat(okHttpClient.callTimeoutMillis())
- .isEqualTo(model1.settings!!.networkTimeout)
- assertThat(httpRequest.header("X-Test-Header"))
- .isEqualTo("Hello, World!")
- }
-
- @Test fun performValidation_success_withCustomSslCert() = runBlocking {
- val response = fakeResponse(200, "OK", "Hello World")
- val call = mock {
- on { execute() } doReturn response
- }
- whenever(okHttpClient.newCall(any())).doReturn(call)
-
- val model1 = fakeModel(id = 1).copy(
- url = "http://wwww.mysite.com/test.html",
- headers = emptyList()
- )
- model1.settings = model1.settings!!.copy(
- certificate = "file:///sdcard/cert.pem"
- )
- val result = manager.performValidation(model1)
-
- assertThat(result.model).isEqualTo(
- model1.copy(
- lastResult = model1.lastResult?.copy(
- status = OK,
- reason = null
- )
- )
- )
- assertThat(okHttpClient.callTimeoutMillis())
- .isEqualTo(model1.settings!!.networkTimeout)
-
- verify(sslManager).clientForCertificate(
- "file:///sdcard/cert.pem",
- "http://wwww.mysite.com/test.html",
- okHttpClient
- )
- }
-
- private fun fakeResponse(
- code: Int,
- message: String,
- body: String?
- ): Response {
- val responseBody = if (body != null) {
- ResponseBody.create(null, body)
- } else {
- null
- }
- val request = Request.Builder()
- .url("https://placeholder.com")
- .build()
- return Response.Builder()
- .protocol(HTTP_2)
- .request(request)
- .message(message)
- .code(code)
- .body(responseBody)
- .build()
- }
-
- private fun fakeJob(id: Int): JobInfo {
- return mock {
- on { this.id } doReturn id
- }
- }
-}
diff --git a/gradle.properties b/gradle.properties
index c62d03d..9516cf5 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -16,6 +16,3 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryErro
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
-
-android.useAndroidX=true
-android.enableJetifier=true
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 2128899..cf2d737 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Fri Jan 05 14:44:01 CST 2018
+#Wed Aug 24 19:51:45 CDT 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
diff --git a/ic_web.png b/ic_web.png
index d54b6ec..509177f 100644
Binary files a/ic_web.png and b/ic_web.png differ
diff --git a/mock/mock-google-services.json b/mock/mock-google-services.json
deleted file mode 100644
index d9866d8..0000000
--- a/mock/mock-google-services.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- "project_info": {
- "project_number": "123456789000",
- "firebase_url": "https://mockproject-1234.firebaseio.com",
- "project_id": "mockproject-1234",
- "storage_bucket": "mockproject-1234.appspot.com"
- },
- "client": [
- {
- "client_info": {
- "mobilesdk_app_id": "1:123456789000:android:f1bf012572b04063",
- "android_client_info": {
- "package_name": "com.afollestad.nocknock"
- }
- },
- "oauth_client": [
- {
- "client_id": "123456789000-hjugbg6ud799v4c49dim8ce2usclthar.apps.googleusercontent.com",
- "client_type": 3
- }
- ],
- "api_key": [
- {
- "current_key": "AIzbSzCn1N6LWIe6wthYyrgUUSAlUsdqMb-wvTo"
- }
- ],
- "services": {
- "analytics_service": {
- "status": 1
- },
- "appinvite_service": {
- "status": 1,
- "other_platform_oauth_client": []
- },
- "ads_service": {
- "status": 2
- }
- }
- }
- ],
- "configuration_version": "1"
-}
diff --git a/mock/mock.gradle b/mock/mock.gradle
deleted file mode 100644
index 2fa58ef..0000000
--- a/mock/mock.gradle
+++ /dev/null
@@ -1,13 +0,0 @@
-// This script must be applied in app/build.gradle for the paths here to work correctly
-
-def copyMockFilesNeeded() {
- def srcGoogleServicesFile = file("../mock/mock-google-services.json")
- def destGoogleServicesFile = file("google-services.json")
- if (!destGoogleServicesFile.exists()) {
- destGoogleServicesFile.write(srcGoogleServicesFile.text)
- }
-}
-
-afterEvaluate {
- copyMockFilesNeeded()
-}
\ No newline at end of file
diff --git a/notifications/.gitignore b/notifications/.gitignore
deleted file mode 100644
index 796b96d..0000000
--- a/notifications/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
diff --git a/notifications/build.gradle b/notifications/build.gradle
deleted file mode 100644
index 5cfdc84..0000000
--- a/notifications/build.gradle
+++ /dev/null
@@ -1,35 +0,0 @@
-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(':common')
-
- api 'androidx.appcompat:appcompat:' + versions.androidxCore
-
- 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 'org.koin:koin-android:' + versions.koin
-
- implementation 'com.jakewharton.timber:timber:' + versions.timber
-
- 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'
\ No newline at end of file
diff --git a/notifications/src/main/AndroidManifest.xml b/notifications/src/main/AndroidManifest.xml
deleted file mode 100644
index 34182e8..0000000
--- a/notifications/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
diff --git a/notifications/src/main/java/com/afollestad/nocknock/notifications/Channel.kt b/notifications/src/main/java/com/afollestad/nocknock/notifications/Channel.kt
deleted file mode 100644
index 7cad6ca..0000000
--- a/notifications/src/main/java/com/afollestad/nocknock/notifications/Channel.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.notifications
-
-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
-) {
- ValidationSuccess(
- id = "check_success",
- title = R.string.channel_server_check_success_title,
- description = R.string.channel_server_check_success_description,
- importance = IMPORTANCE_DEFAULT
- ),
- ValidationErrors(
- id = "check_failures",
- title = R.string.channel_server_check_failures_title,
- description = R.string.channel_server_check_failures_description,
- importance = IMPORTANCE_DEFAULT
- )
-}
diff --git a/notifications/src/main/java/com/afollestad/nocknock/notifications/NockNotificationManager.kt b/notifications/src/main/java/com/afollestad/nocknock/notifications/NockNotificationManager.kt
deleted file mode 100644
index 84addbc..0000000
--- a/notifications/src/main/java/com/afollestad/nocknock/notifications/NockNotificationManager.kt
+++ /dev/null
@@ -1,129 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.notifications
-
-import android.annotation.TargetApi
-import android.app.NotificationManager
-import android.os.Build.VERSION_CODES
-import com.afollestad.nocknock.notifications.Channel.ValidationErrors
-import com.afollestad.nocknock.utilities.providers.CanNotifyModel
-import com.afollestad.nocknock.utilities.providers.IntentProvider
-import com.afollestad.nocknock.utilities.providers.NotificationChannelProvider
-import com.afollestad.nocknock.utilities.providers.NotificationProvider
-import com.afollestad.nocknock.utilities.providers.RealIntentProvider.Companion.BASE_NOTIFICATION_REQUEST_CODE
-import com.afollestad.nocknock.utilities.providers.StringProvider
-import timber.log.Timber.d as log
-
-/** @author Aidan Follestad (@afollestad) */
-interface NockNotificationManager {
-
- fun setIsAppOpen(open: Boolean)
-
- fun createChannels()
-
- fun postValidationErrorNotification(model: CanNotifyModel)
-
- fun postValidationSuccessNotification(model: CanNotifyModel)
-
- fun cancelStatusNotification(model: CanNotifyModel)
-
- fun cancelStatusNotifications()
-}
-
-/** @author Aidan Follestad (@afollestad) */
-class RealNockNotificationManager(
- private val stockManager: NotificationManager,
- private val stringProvider: StringProvider,
- private val intentProvider: IntentProvider,
- private val channelProvider: NotificationChannelProvider,
- private val notificationProvider: NotificationProvider
-) : NockNotificationManager {
-
- private var isAppOpen = false
-
- override fun setIsAppOpen(open: Boolean) {
- this.isAppOpen = open
- log("Is app open? $open")
- }
-
- override fun createChannels() =
- Channel.values().forEach(this::createChannel)
-
- override fun postValidationErrorNotification(model: CanNotifyModel) {
- if (isAppOpen) {
- // Don't show notifications while the app is open
- log("App is open, validation error notification for site ${model.notifyId()} won't be posted.")
- return
- }
-
- log("Posting validation error notification for site ${model.notifyId()}...")
- val intent = intentProvider.getPendingIntentForViewSite(model)
-
- val newNotification = notificationProvider.create(
- channelId = ValidationErrors.id,
- title = model.notifyName(),
- content = model.notifyDescription() ?: stringProvider.get(R.string.something_wrong),
- intent = intent,
- smallIcon = R.drawable.ic_notification_error
- )
-
- stockManager.notify(model.notifyTag(), model.notificationId(), newNotification)
- log("Posted validation error notification for site ${model.notificationId()}.")
- }
-
- override fun postValidationSuccessNotification(model: CanNotifyModel) {
- if (isAppOpen) {
- // Don't show notifications while the app is open
- log("App is open, validation success notification for site ${model.notifyId()} won't be posted.")
- return
- }
-
- log("Posting validation success notification for site ${model.notifyId()}...")
- val intent = intentProvider.getPendingIntentForViewSite(model)
-
- val newNotification = notificationProvider.create(
- channelId = ValidationErrors.id,
- title = model.notifyName(),
- content = stringProvider.get(R.string.validation_passed),
- intent = intent,
- smallIcon = R.drawable.ic_notification_success
- )
-
- stockManager.notify(model.notifyTag(), model.notificationId(), newNotification)
- log("Posted validation success notification for site ${model.notificationId()}.")
- }
-
- override fun cancelStatusNotification(model: CanNotifyModel) {
- stockManager.cancel(model.notificationId())
- log("Cancelled status notification for site ${model.notifyId()}.")
- }
-
- override fun cancelStatusNotifications() = stockManager.cancelAll()
-
- @TargetApi(VERSION_CODES.O)
- private fun createChannel(channel: Channel) {
- val notificationChannel = channelProvider.create(
- id = channel.id,
- title = stringProvider.get(channel.title),
- description = stringProvider.get(channel.description),
- importance = channel.importance
- )
- notificationChannel?.let(stockManager::createNotificationChannel)
- log("Created notification channel ${channel.id}")
- }
-
- private fun CanNotifyModel.notificationId() = BASE_NOTIFICATION_REQUEST_CODE + this.notifyId()
-}
diff --git a/notifications/src/main/java/com/afollestad/nocknock/notifications/NotificationsModule.kt b/notifications/src/main/java/com/afollestad/nocknock/notifications/NotificationsModule.kt
deleted file mode 100644
index 73274c5..0000000
--- a/notifications/src/main/java/com/afollestad/nocknock/notifications/NotificationsModule.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.notifications
-
-import com.afollestad.nocknock.notifications.Qualifiers.MAIN_ACTIVITY_CLASS
-import com.afollestad.nocknock.utilities.providers.BundleProvider
-import com.afollestad.nocknock.utilities.providers.IntentProvider
-import com.afollestad.nocknock.utilities.providers.JobInfoProvider
-import com.afollestad.nocknock.utilities.providers.NotificationChannelProvider
-import com.afollestad.nocknock.utilities.providers.NotificationProvider
-import com.afollestad.nocknock.utilities.providers.RealBundleProvider
-import com.afollestad.nocknock.utilities.providers.RealIntentProvider
-import com.afollestad.nocknock.utilities.providers.RealJobInfoProvider
-import com.afollestad.nocknock.utilities.providers.RealNotificationChannelProvider
-import com.afollestad.nocknock.utilities.providers.RealNotificationProvider
-import com.afollestad.nocknock.utilities.providers.RealSdkProvider
-import com.afollestad.nocknock.utilities.providers.SdkProvider
-import org.koin.dsl.module.module
-
-object Qualifiers {
- const val MAIN_ACTIVITY_CLASS = "main_activity_class"
-}
-
-val notificationsModule = module {
-
- factory {
- RealIntentProvider(get(), get(name = MAIN_ACTIVITY_CLASS))
- } bind IntentProvider::class
-
- factory { RealSdkProvider() } bind SdkProvider::class
-
- factory {
- RealNotificationChannelProvider(get())
- } bind NotificationChannelProvider::class
-
- factory { RealNotificationProvider(get()) } bind NotificationProvider::class
-
- factory { RealBundleProvider() } bind BundleProvider::class
-
- factory { RealJobInfoProvider(get()) } bind JobInfoProvider::class
-
- single {
- RealNockNotificationManager(
- get(),
- get(),
- get(),
- get(),
- get()
- )
- } bind NockNotificationManager::class
-}
diff --git a/notifications/src/main/res/drawable/ic_notification_error.xml b/notifications/src/main/res/drawable/ic_notification_error.xml
deleted file mode 100644
index df55b3c..0000000
--- a/notifications/src/main/res/drawable/ic_notification_error.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/notifications/src/main/res/drawable/ic_notification_success.xml b/notifications/src/main/res/drawable/ic_notification_success.xml
deleted file mode 100644
index 290fb76..0000000
--- a/notifications/src/main/res/drawable/ic_notification_success.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/notifications/src/main/res/values/strings.xml b/notifications/src/main/res/values/strings.xml
deleted file mode 100644
index 4462309..0000000
--- a/notifications/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
- Site Validation Failures
-
- Notifications for Nock Nock validations failing for your sites. Something has gone
- wrong if you see one of these.
-
-
- Site Validation Success
-
- Notifications for Nock Nock when a site validation passes when it previously had not.
-
-
- Something\'s wrong! Tap for details.
- Yay! No longer in trouble! Validation passed.
-
-
diff --git a/notifications/src/test/java/com/afollestad/nocknock/notifications/NockNotificationManagerTest.kt b/notifications/src/test/java/com/afollestad/nocknock/notifications/NockNotificationManagerTest.kt
deleted file mode 100644
index d4709ab..0000000
--- a/notifications/src/test/java/com/afollestad/nocknock/notifications/NockNotificationManagerTest.kt
+++ /dev/null
@@ -1,175 +0,0 @@
-/**
- * Designed and developed by Aidan Follestad (@afollestad)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.afollestad.nocknock.notifications
-
-import android.app.Notification
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
-import com.afollestad.nocknock.notifications.Channel.ValidationErrors
-import com.afollestad.nocknock.notifications.Channel.ValidationSuccess
-import com.afollestad.nocknock.utilities.providers.CanNotifyModel
-import com.afollestad.nocknock.utilities.providers.IntentProvider
-import com.afollestad.nocknock.utilities.providers.NotificationChannelProvider
-import com.afollestad.nocknock.utilities.providers.NotificationProvider
-import com.afollestad.nocknock.utilities.providers.RealIntentProvider.Companion.BASE_NOTIFICATION_REQUEST_CODE
-import com.afollestad.nocknock.utilities.providers.StringProvider
-import com.google.common.truth.Truth.assertThat
-import com.nhaarman.mockitokotlin2.any
-import com.nhaarman.mockitokotlin2.argumentCaptor
-import com.nhaarman.mockitokotlin2.doAnswer
-import com.nhaarman.mockitokotlin2.doReturn
-import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.times
-import com.nhaarman.mockitokotlin2.verify
-import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
-import com.nhaarman.mockitokotlin2.whenever
-import org.junit.Before
-import org.junit.Test
-
-class NockNotificationManagerTest {
-
- private val somethingWentWrong = "something went wrong"
- private val yay = "yay!"
-
- private val stockManager = mock()
- private val stringProvider = mock {
- on { get(R.string.something_wrong) } doReturn somethingWentWrong
- on { get(R.string.validation_passed) } doReturn yay
- }
- private val intentProvider = mock()
- private val channelProvider = mock()
- private val notificationProvider = mock()
-
- private val manager = RealNockNotificationManager(
- stockManager,
- stringProvider,
- intentProvider,
- channelProvider,
- notificationProvider
- )
-
- @Before fun setup() {
- whenever(channelProvider.create(any(), any(), any(), any())).doAnswer { inv ->
- val id = inv.getArgument(0)
- val title = inv.getArgument(1)
- val description = inv.getArgument(2)
- val important = inv.getArgument(3)
- return@doAnswer mock {
- on { this.id } doReturn id
- on { this.name } doReturn title
- on { this.description } doReturn description
- on { this.importance } doReturn important
- }
- }
- }
-
- @Test fun createChannels() {
- whenever(stringProvider.get(any())).doReturn("")
- val errorChannel = mock