diff --git a/app/build.gradle b/app/build.gradle
index aad5e53..0a8d678 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -18,9 +18,9 @@ android {
}
dependencies {
- implementation project(':data')
implementation project(':utilities')
implementation project(':engine')
+ implementation project(':data')
implementation project(':notifications')
implementation project(':viewcomponents')
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 87a00d8..c0b8ee5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -42,7 +42,7 @@
android:windowSoftInputMode="stateHidden"/>
diff --git a/app/src/main/java/com/afollestad/nocknock/NockNockApp.kt b/app/src/main/java/com/afollestad/nocknock/NockNockApp.kt
index c1cf284..492ce0e 100644
--- a/app/src/main/java/com/afollestad/nocknock/NockNockApp.kt
+++ b/app/src/main/java/com/afollestad/nocknock/NockNockApp.kt
@@ -19,7 +19,7 @@ import android.app.Application
import com.afollestad.nocknock.di.AppComponent
import com.afollestad.nocknock.di.DaggerAppComponent
import com.afollestad.nocknock.engine.statuscheck.BootReceiver
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob
+import com.afollestad.nocknock.engine.statuscheck.ValidationJob
import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
import com.afollestad.nocknock.ui.main.MainActivity
@@ -82,7 +82,7 @@ class NockNockApp : Application(), Injector {
is MainActivity -> appComponent.inject(target)
is ViewSiteActivity -> appComponent.inject(target)
is AddSiteActivity -> appComponent.inject(target)
- is CheckStatusJob -> appComponent.inject(target)
+ is ValidationJob -> appComponent.inject(target)
is BootReceiver -> appComponent.inject(target)
else -> throw IllegalStateException("Can't inject into $target")
}
diff --git a/app/src/main/java/com/afollestad/nocknock/adapter/ServerAdapter.kt b/app/src/main/java/com/afollestad/nocknock/adapter/SiteAdapter.kt
similarity index 74%
rename from app/src/main/java/com/afollestad/nocknock/adapter/ServerAdapter.kt
rename to app/src/main/java/com/afollestad/nocknock/adapter/SiteAdapter.kt
index c87760e..a519e5a 100644
--- a/app/src/main/java/com/afollestad/nocknock/adapter/ServerAdapter.kt
+++ b/app/src/main/java/com/afollestad/nocknock/adapter/SiteAdapter.kt
@@ -20,9 +20,10 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.nocknock.R
-import com.afollestad.nocknock.data.ServerModel
-import com.afollestad.nocknock.data.isPending
-import com.afollestad.nocknock.data.textRes
+import com.afollestad.nocknock.data.model.Site
+import com.afollestad.nocknock.data.model.Status.WAITING
+import com.afollestad.nocknock.data.model.isPending
+import com.afollestad.nocknock.data.model.textRes
import com.afollestad.nocknock.utilities.ui.onDebouncedClick
import kotlinx.android.synthetic.main.list_item_server.view.iconStatus
import kotlinx.android.synthetic.main.list_item_server.view.textInterval
@@ -30,10 +31,10 @@ import kotlinx.android.synthetic.main.list_item_server.view.textName
import kotlinx.android.synthetic.main.list_item_server.view.textStatus
import kotlinx.android.synthetic.main.list_item_server.view.textUrl
-typealias Listener = (model: ServerModel, longClick: Boolean) -> Unit
+typealias Listener = (model: Site, longClick: Boolean) -> Unit
/** @author Aidan Follestad (@afollestad) */
-class ServerVH constructor(
+class SiteViewHolder constructor(
itemView: View,
private val adapter: ServerAdapter
) : RecyclerView.ViewHolder(itemView), View.OnLongClickListener {
@@ -45,24 +46,32 @@ class ServerVH constructor(
itemView.setOnLongClickListener(this)
}
- fun bind(model: ServerModel) {
+ fun bind(model: Site) {
+ requireNotNull(model.settings) { "Settings must be populated." }
+
itemView.textName.text = model.name
itemView.textUrl.text = model.url
- itemView.iconStatus.setStatus(model.status)
- val statusText = model.status.textRes()
- if (statusText == 0) {
- itemView.textStatus.text = model.reason
+ val lastResult = model.lastResult
+ if (lastResult != null) {
+ itemView.iconStatus.setStatus(lastResult.status)
+ val statusText = lastResult.status.textRes()
+ if (statusText == 0) {
+ itemView.textStatus.text = lastResult.reason
+ } else {
+ itemView.textStatus.setText(statusText)
+ }
} else {
- itemView.textStatus.setText(statusText)
+ itemView.iconStatus.setStatus(WAITING)
+ itemView.textStatus.setText(R.string.none)
}
val res = itemView.resources
when {
- model.disabled -> {
+ model.settings?.disabled == true -> {
itemView.textInterval.setText(R.string.checks_disabled)
}
- model.status.isPending() -> {
+ model.lastResult?.status.isPending() -> {
itemView.textInterval.text = res.getString(
R.string.next_check_x,
res.getString(R.string.now)
@@ -84,21 +93,21 @@ class ServerVH constructor(
}
/** @author Aidan Follestad (@afollestad) */
-class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter() {
+class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter() {
- private val models = mutableListOf()
+ private val models = mutableListOf()
internal fun performClick(
index: Int,
longClick: Boolean
) = listener.invoke(models[index], longClick)
- fun add(model: ServerModel) {
+ fun add(model: Site) {
models.add(model)
notifyItemInserted(models.size - 1)
}
- fun update(target: ServerModel) {
+ fun update(target: Site) {
for ((i, model) in models.withIndex()) {
if (model.id == target.id) {
update(i, target)
@@ -109,7 +118,7 @@ class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter) {
+ fun set(newModels: List) {
this.models.clear()
if (!newModels.isEmpty()) {
this.models.addAll(newModels)
@@ -140,14 +149,14 @@ class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter provideMainActivityClass() {
+ return MainActivity.class;
+ }
+
+ @Provides
+ @Singleton
+ static AppDatabase provideAppDatabase(Application app) {
+ return databaseBuilder(app, AppDatabase.class, DATABASE_NAME).build();
+ }
+}
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
index 0a99070..48d0671 100644
--- a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt
+++ b/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt
@@ -20,11 +20,11 @@ import android.os.Bundle
import android.widget.ArrayAdapter
import androidx.appcompat.app.AppCompatActivity
import com.afollestad.nocknock.R
-import com.afollestad.nocknock.data.ValidationMode
-import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
-import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
-import com.afollestad.nocknock.data.indexToValidationMode
+import com.afollestad.nocknock.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.indexToValidationMode
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
import com.afollestad.nocknock.utilities.ext.injector
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
@@ -119,7 +119,7 @@ class AddSiteActivity : AppCompatActivity(), AddSiteView {
url = inputUrl.trimmedText(),
checkInterval = checkInterval,
validationMode = validationMode,
- validationContent = validationMode.validationContent(),
+ validationArgs = validationMode.validationContent(),
networkTimeout = responseTimeoutInput.textAsInt(defaultValue = defaultTimeout)
)
}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSitePresenter.kt b/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSitePresenter.kt
index aff0c90..45fc9f3 100644
--- a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSitePresenter.kt
+++ b/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSitePresenter.kt
@@ -17,13 +17,14 @@ package com.afollestad.nocknock.ui.addsite
import androidx.annotation.CheckResult
import com.afollestad.nocknock.R
-import com.afollestad.nocknock.data.ServerModel
-import com.afollestad.nocknock.data.ServerStatus.WAITING
-import com.afollestad.nocknock.data.ValidationMode
-import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
-import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
-import com.afollestad.nocknock.engine.db.ServerModelStore
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
+import com.afollestad.nocknock.data.AppDatabase
+import com.afollestad.nocknock.data.model.Site
+import com.afollestad.nocknock.data.model.SiteSettings
+import com.afollestad.nocknock.data.model.ValidationMode
+import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
+import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
+import com.afollestad.nocknock.data.putSite
+import com.afollestad.nocknock.engine.statuscheck.ValidationManager
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.async
@@ -63,7 +64,7 @@ interface AddSitePresenter {
url: String,
checkInterval: Long,
validationMode: ValidationMode,
- validationContent: String?,
+ validationArgs: String?,
networkTimeout: Int
)
@@ -72,8 +73,8 @@ interface AddSitePresenter {
/** @author Aidan Follestad (@afollestad) */
class RealAddSitePresenter @Inject constructor(
- private val serverModelStore: ServerModelStore,
- private val checkStatusManager: CheckStatusManager
+ private val database: AppDatabase,
+ private val checkStatusManager: ValidationManager
) : AddSitePresenter {
private var view: AddSiteView? = null
@@ -118,7 +119,7 @@ class RealAddSitePresenter @Inject constructor(
url: String,
checkInterval: Long,
validationMode: ValidationMode,
- validationContent: String?,
+ validationArgs: String?,
networkTimeout: Int
) {
val inputErrors = InputErrors()
@@ -134,9 +135,9 @@ class RealAddSitePresenter @Inject constructor(
if (checkInterval <= 0) {
inputErrors.checkInterval = R.string.please_enter_check_interval
}
- if (validationMode == TERM_SEARCH && validationContent.isNullOrEmpty()) {
+ if (validationMode == TERM_SEARCH && validationArgs.isNullOrEmpty()) {
inputErrors.termSearch = R.string.please_enter_search_term
- } else if (validationMode == JAVASCRIPT && validationContent.isNullOrEmpty()) {
+ } else if (validationMode == JAVASCRIPT && validationArgs.isNullOrEmpty()) {
inputErrors.javaScript = R.string.please_enter_javaScript
}
if (networkTimeout <= 0) {
@@ -148,14 +149,19 @@ class RealAddSitePresenter @Inject constructor(
return
}
- val newModel = ServerModel(
+ val newSettings = SiteSettings(
+ validationIntervalMs = checkInterval,
+ validationMode = validationMode,
+ validationArgs = validationArgs,
+ networkTimeout = networkTimeout,
+ disabled = false
+ )
+ val newModel = Site(
+ id = 0,
name = name,
url = url,
- status = WAITING,
- checkInterval = checkInterval,
- validationMode = validationMode,
- validationContent = validationContent,
- networkTimeout = networkTimeout
+ settings = newSettings,
+ lastResult = null
)
with(view!!) {
@@ -163,7 +169,7 @@ class RealAddSitePresenter @Inject constructor(
launch(coroutineContext) {
setLoading()
val storedModel = async(IO) {
- serverModelStore.put(newModel)
+ database.putSite(newModel)
}.await()
checkStatusManager.scheduleCheck(
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
index b77de55..3ff7999 100644
--- a/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivity.kt
+++ b/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivity.kt
@@ -28,9 +28,9 @@ import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems
import com.afollestad.nocknock.R
import com.afollestad.nocknock.adapter.ServerAdapter
-import com.afollestad.nocknock.data.ServerModel
+import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.dialogs.AboutDialog
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
+import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.ACTION_STATUS_UPDATE
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
import com.afollestad.nocknock.utilities.ext.injector
import com.afollestad.nocknock.utilities.ext.safeRegisterReceiver
@@ -39,6 +39,7 @@ import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
import kotlinx.android.synthetic.main.activity_main.fab
import kotlinx.android.synthetic.main.activity_main.list
+import kotlinx.android.synthetic.main.activity_main.loadingProgress
import kotlinx.android.synthetic.main.activity_main.rootView
import kotlinx.android.synthetic.main.activity_main.toolbar
import kotlinx.android.synthetic.main.include_empty_view.emptyText
@@ -109,18 +110,22 @@ class MainActivity : AppCompatActivity(), MainView {
super.onDestroy()
}
- override fun setModels(models: List) {
+ override fun setLoading() = loadingProgress.setLoading()
+
+ override fun setDoneLoading() = loadingProgress.setDone()
+
+ override fun setModels(models: List) {
list.post {
adapter.set(models)
emptyText.showOrHide(models.isEmpty())
}
}
- override fun updateModel(model: ServerModel) {
+ override fun updateModel(model: Site) {
list.post { adapter.update(model) }
}
- override fun onSiteDeleted(model: ServerModel) {
+ override fun onSiteDeleted(model: Site) {
list.post {
adapter.remove(model)
emptyText.showOrHide(adapter.itemCount == 0)
@@ -133,7 +138,7 @@ class MainActivity : AppCompatActivity(), MainView {
) = rootView.scopeWhileAttached(context, exec)
private fun onSiteSelected(
- model: ServerModel,
+ model: Site,
longClick: Boolean
) {
if (longClick) {
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
index e9780bf..acfbfd4 100644
--- a/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivityExt.kt
+++ b/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivityExt.kt
@@ -18,7 +18,7 @@ package com.afollestad.nocknock.ui.main
import android.content.Intent
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.nocknock.R
-import com.afollestad.nocknock.data.ServerModel
+import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.toHtml
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
import com.afollestad.nocknock.ui.addsite.KEY_FAB_SIZE
@@ -47,16 +47,16 @@ private fun MainActivity.intentToAdd(
addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
}
-internal fun MainActivity.viewSite(model: ServerModel) {
+internal fun MainActivity.viewSite(model: Site) {
startActivityForResult(intentToView(model), VIEW_SITE_RQ)
}
-private fun MainActivity.intentToView(model: ServerModel) =
+private fun MainActivity.intentToView(model: Site) =
Intent(this, ViewSiteActivity::class.java).apply {
putExtra(KEY_VIEW_MODEL, model)
}
-internal fun MainActivity.maybeRemoveSite(model: ServerModel) {
+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())
@@ -67,7 +67,7 @@ internal fun MainActivity.maybeRemoveSite(model: ServerModel) {
internal fun MainActivity.processIntent(intent: Intent) {
if (intent.hasExtra(KEY_VIEW_NOTIFICATION_MODEL)) {
- val model = intent.getSerializableExtra(KEY_VIEW_NOTIFICATION_MODEL) as ServerModel
+ val model = intent.getSerializableExtra(KEY_VIEW_NOTIFICATION_MODEL) as Site
viewSite(model)
}
}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/main/MainPresenter.kt b/app/src/main/java/com/afollestad/nocknock/ui/main/MainPresenter.kt
index 9f3fd8d..efd9c64 100644
--- a/app/src/main/java/com/afollestad/nocknock/ui/main/MainPresenter.kt
+++ b/app/src/main/java/com/afollestad/nocknock/ui/main/MainPresenter.kt
@@ -15,18 +15,25 @@
*/
package com.afollestad.nocknock.ui.main
+import android.app.Application
+import android.content.Context.MODE_PRIVATE
import android.content.Intent
-import com.afollestad.nocknock.data.ServerModel
-import com.afollestad.nocknock.engine.db.ServerModelStore
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
+import com.afollestad.nocknock.data.AppDatabase
+import com.afollestad.nocknock.data.allSites
+import com.afollestad.nocknock.data.deleteSite
+import com.afollestad.nocknock.data.legacy.DbMigrator
+import com.afollestad.nocknock.data.model.Site
+import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.ACTION_STATUS_UPDATE
+import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.KEY_UPDATE_MODEL
+import com.afollestad.nocknock.engine.statuscheck.ValidationManager
import com.afollestad.nocknock.notifications.NockNotificationManager
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import javax.inject.Inject
+import timber.log.Timber.d as log
/** @author Aidan Follestad (@afollestad) */
interface MainPresenter {
@@ -37,18 +44,19 @@ interface MainPresenter {
fun resume()
- fun refreshSite(site: ServerModel)
+ fun refreshSite(site: Site)
- fun removeSite(site: ServerModel)
+ fun removeSite(site: Site)
fun dropView()
}
/** @author Aidan Follestad (@afollestad) */
class RealMainPresenter @Inject constructor(
- private val serverModelStore: ServerModelStore,
+ private val app: Application,
+ private val database: AppDatabase,
private val notificationManager: NockNotificationManager,
- private val checkStatusManager: CheckStatusManager
+ private val checkStatusManager: ValidationManager
) : MainPresenter {
private var view: MainView? = null
@@ -61,40 +69,46 @@ class RealMainPresenter @Inject constructor(
override fun onBroadcast(intent: Intent) {
if (intent.action == ACTION_STATUS_UPDATE) {
- val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? ServerModel ?: return
+ val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? Site
+ ?: return
view?.updateModel(model)
}
}
override fun resume() {
notificationManager.cancelStatusNotifications()
+
view!!.run {
setModels(listOf())
+ setLoading()
+
scopeWhileAttached(Main) {
launch(coroutineContext) {
- val models = async(IO) {
- serverModelStore.get()
- }.await()
+ doMigrationIfNeeded()
+
+ val models = async(IO) { database.allSites() }.await()
+
setModels(models)
+ setDoneLoading()
}
}
}
}
- override fun refreshSite(site: ServerModel) {
+ override fun refreshSite(site: Site) =
checkStatusManager.scheduleCheck(
site = site,
rightNow = true,
cancelPrevious = true
)
- }
- override fun removeSite(site: ServerModel) {
+ override fun removeSite(site: Site) {
checkStatusManager.cancelCheck(site)
notificationManager.cancelStatusNotification(site)
+
view!!.scopeWhileAttached(Main) {
launch(coroutineContext) {
- async(IO) { serverModelStore.delete(site) }.await()
+ async(IO) { database.deleteSite(site) }.await()
view?.onSiteDeleted(site)
}
}
@@ -104,6 +118,28 @@ class RealMainPresenter @Inject constructor(
view = null
}
+ private suspend fun CoroutineScope.doMigrationIfNeeded() {
+ if (needDbMigration()) {
+ log("Doing database migration...")
+ val migratedCount = async(IO) {
+ DbMigrator(app, database).migrateAll()
+ }.await()
+ didDbMigration()
+ log("Database migration done! Migrated $migratedCount models.")
+ ensureCheckJobs()
+ }
+ }
+
+ private fun needDbMigration(): Boolean =
+ !app.getSharedPreferences("settings", MODE_PRIVATE)
+ .getBoolean("did_db_migration", false)
+
+ private fun didDbMigration() =
+ app.getSharedPreferences("settings", MODE_PRIVATE)
+ .edit()
+ .putBoolean("did_db_migration", true)
+ .apply()
+
private fun ensureCheckJobs() {
view!!.scopeWhileAttached(IO) {
launch(coroutineContext) {
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/main/MainView.kt b/app/src/main/java/com/afollestad/nocknock/ui/main/MainView.kt
index d0dfa71..a8004d2 100644
--- a/app/src/main/java/com/afollestad/nocknock/ui/main/MainView.kt
+++ b/app/src/main/java/com/afollestad/nocknock/ui/main/MainView.kt
@@ -15,18 +15,22 @@
*/
package com.afollestad.nocknock.ui.main
-import com.afollestad.nocknock.data.ServerModel
+import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
import kotlin.coroutines.CoroutineContext
/** @author Aidan Follestad (@afollestad) */
interface MainView {
- fun setModels(models: List)
+ fun setLoading()
- fun updateModel(model: ServerModel)
+ fun setDoneLoading()
- fun onSiteDeleted(model: ServerModel)
+ fun setModels(models: List)
+
+ fun updateModel(model: Site)
+
+ fun onSiteDeleted(model: Site)
fun scopeWhileAttached(
context: CoroutineContext,
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
index 8dc66a8..b383aeb 100644
--- a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt
+++ b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt
@@ -24,15 +24,15 @@ import android.os.Bundle
import android.widget.ArrayAdapter
import androidx.appcompat.app.AppCompatActivity
import com.afollestad.nocknock.R
-import com.afollestad.nocknock.data.LAST_CHECK_NONE
-import com.afollestad.nocknock.data.ServerModel
-import com.afollestad.nocknock.data.ValidationMode
-import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
-import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
-import com.afollestad.nocknock.data.indexToValidationMode
-import com.afollestad.nocknock.data.textRes
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
+import com.afollestad.nocknock.data.model.Site
+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.indexToValidationMode
+import com.afollestad.nocknock.data.model.textRes
+import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.ACTION_STATUS_UPDATE
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
import com.afollestad.nocknock.utilities.ext.formatDate
import com.afollestad.nocknock.utilities.ext.injector
@@ -63,6 +63,7 @@ import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
import kotlinx.android.synthetic.main.activity_viewsite.toolbar
import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription
+import java.lang.System.currentTimeMillis
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
@@ -132,7 +133,7 @@ class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
url = inputUrl.trimmedText(),
checkInterval = checkInterval,
validationMode = validationMode,
- validationContent = validationMode.validationContent(),
+ validationArgs = validationMode.validationContent(),
networkTimeout = responseTimeoutInput.textAsInt(defaultValue = defaultTimeout)
)
}
@@ -170,44 +171,48 @@ class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
override fun setValidationModeDescription(res: Int) = validationModeDescription.setText(res)
- override fun displayModel(model: ServerModel) = with(model) {
- iconStatus.setStatus(this.status)
+ override fun displayModel(model: Site) = with(model) {
+ val siteSettings = this.settings
+ requireNotNull(siteSettings) { "Site settings must be populated." }
+
+ iconStatus.setStatus(this.lastResult?.status ?: WAITING)
inputName.setText(this.name)
inputUrl.setText(this.url)
- if (this.lastCheck == LAST_CHECK_NONE) {
+ if (this.lastResult == null) {
textLastCheckResult.setText(R.string.none)
} else {
- val statusText = this.status.textRes()
+ val statusText = this.lastResult!!.status.textRes()
textLastCheckResult.text = if (statusText == 0) {
- this.reason
+ this.lastResult!!.reason
} else {
getString(statusText)
}
}
- if (this.disabled) {
+ if (siteSettings.disabled) {
textNextCheck.setText(R.string.auto_checks_disabled)
} else {
- textNextCheck.text = (this.lastCheck + this.checkInterval).formatDate()
+ val lastCheck = this.lastResult?.timestampMs ?: currentTimeMillis()
+ textNextCheck.text = (lastCheck + siteSettings.validationIntervalMs).formatDate()
}
- checkIntervalLayout.set(this.checkInterval)
+ checkIntervalLayout.set(siteSettings.validationIntervalMs)
- responseValidationMode.setSelection(validationMode.value - 1)
- when (this.validationMode) {
- TERM_SEARCH -> responseValidationSearchTerm.setText(this.validationContent ?: "")
- JAVASCRIPT -> scriptInputLayout.setCode(this.validationContent)
+ responseValidationMode.setSelection(siteSettings.validationMode.value - 1)
+ when (siteSettings.validationMode) {
+ TERM_SEARCH -> responseValidationSearchTerm.setText(siteSettings.validationArgs ?: "")
+ JAVASCRIPT -> scriptInputLayout.setCode(siteSettings.validationArgs)
else -> {
responseValidationSearchTerm.setText("")
scriptInputLayout.clear()
}
}
- responseTimeoutInput.setText(model.networkTimeout.toString())
+ responseTimeoutInput.setText(siteSettings.networkTimeout.toString())
- disableChecksButton.showOrHide(!this.disabled)
+ disableChecksButton.showOrHide(!siteSettings.disabled)
doneBtn.setText(
- if (this.disabled) R.string.renable_and_save_changes
+ if (siteSettings.disabled) R.string.renable_and_save_changes
else R.string.save_changes
)
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
index 063fbcc..6888d76 100644
--- a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivityExt.kt
+++ b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivityExt.kt
@@ -18,8 +18,8 @@ package com.afollestad.nocknock.ui.viewsite
import android.widget.ImageView
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.nocknock.R
-import com.afollestad.nocknock.data.ServerModel
-import com.afollestad.nocknock.data.isPending
+import com.afollestad.nocknock.data.model.Site
+import com.afollestad.nocknock.data.model.isPending
import com.afollestad.nocknock.toHtml
import com.afollestad.nocknock.utilities.ext.animateRotation
import kotlinx.android.synthetic.main.activity_viewsite.toolbar
@@ -46,11 +46,11 @@ internal fun ViewSiteActivity.maybeDisableChecks() {
}
}
-internal fun ViewSiteActivity.invalidateMenuForStatus(model: ServerModel) {
+internal fun ViewSiteActivity.invalidateMenuForStatus(model: Site) {
val refreshIcon = toolbar.menu.findItem(R.id.refresh)
.actionView as ImageView
- if (model.status.isPending()) {
+ if (model.lastResult?.status.isPending()) {
refreshIcon.animateRotation()
} else {
refreshIcon.run {
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSitePresenter.kt b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSitePresenter.kt
index 8c66570..4f936d2 100644
--- a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSitePresenter.kt
+++ b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSitePresenter.kt
@@ -18,15 +18,17 @@ package com.afollestad.nocknock.ui.viewsite
import android.content.Intent
import androidx.annotation.CheckResult
import com.afollestad.nocknock.R
-import com.afollestad.nocknock.data.ServerModel
-import com.afollestad.nocknock.data.ServerStatus.WAITING
-import com.afollestad.nocknock.data.ValidationMode
-import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
-import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
-import com.afollestad.nocknock.engine.db.ServerModelStore
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
+import com.afollestad.nocknock.data.AppDatabase
+import com.afollestad.nocknock.data.deleteSite
+import com.afollestad.nocknock.data.model.Site
+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.TERM_SEARCH
+import com.afollestad.nocknock.data.updateSite
+import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.ACTION_STATUS_UPDATE
+import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.KEY_UPDATE_MODEL
+import com.afollestad.nocknock.engine.statuscheck.ValidationManager
import com.afollestad.nocknock.notifications.NockNotificationManager
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
@@ -77,7 +79,7 @@ interface ViewSitePresenter {
url: String,
checkInterval: Long,
validationMode: ValidationMode,
- validationContent: String?,
+ validationArgs: String?,
networkTimeout: Int
)
@@ -87,26 +89,26 @@ interface ViewSitePresenter {
fun removeSite()
- fun currentModel(): ServerModel
+ fun currentModel(): Site
fun dropView()
}
/** @author Aidan Follestad (@afollestad) */
class RealViewSitePresenter @Inject constructor(
- private val serverModelStore: ServerModelStore,
- private val checkStatusManager: CheckStatusManager,
+ private val database: AppDatabase,
+ private val checkStatusManager: ValidationManager,
private val notificationManager: NockNotificationManager
) : ViewSitePresenter {
private var view: ViewSiteView? = null
- private var currentModel: ServerModel? = null
+ private var currentModel: Site? = null
override fun takeView(
view: ViewSiteView,
intent: Intent
) {
- this.currentModel = intent.getSerializableExtra(KEY_VIEW_MODEL) as ServerModel
+ this.currentModel = intent.getSerializableExtra(KEY_VIEW_MODEL) as Site
this.view = view.apply {
displayModel(currentModel!!)
}
@@ -114,7 +116,8 @@ class RealViewSitePresenter @Inject constructor(
override fun onBroadcast(intent: Intent) {
if (intent.action == ACTION_STATUS_UPDATE) {
- val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? ServerModel ?: return
+ val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? Site
+ ?: return
this.currentModel = model
view?.displayModel(model)
}
@@ -122,7 +125,7 @@ class RealViewSitePresenter @Inject constructor(
override fun onNewIntent(intent: Intent?) {
if (intent != null && intent.hasExtra(KEY_VIEW_MODEL)) {
- currentModel = intent.getSerializableExtra(KEY_VIEW_MODEL) as ServerModel
+ currentModel = intent.getSerializableExtra(KEY_VIEW_MODEL) as Site
view?.displayModel(currentModel!!)
}
}
@@ -163,7 +166,7 @@ class RealViewSitePresenter @Inject constructor(
url: String,
checkInterval: Long,
validationMode: ValidationMode,
- validationContent: String?,
+ validationArgs: String?,
networkTimeout: Int
) {
val inputErrors = InputErrors()
@@ -179,9 +182,9 @@ class RealViewSitePresenter @Inject constructor(
if (checkInterval <= 0) {
inputErrors.checkInterval = R.string.please_enter_check_interval
}
- if (validationMode == TERM_SEARCH && validationContent.isNullOrEmpty()) {
+ if (validationMode == TERM_SEARCH && validationArgs.isNullOrEmpty()) {
inputErrors.termSearch = R.string.please_enter_search_term
- } else if (validationMode == JAVASCRIPT && validationContent.isNullOrEmpty()) {
+ } else if (validationMode == JAVASCRIPT && validationArgs.isNullOrEmpty()) {
inputErrors.javaScript = R.string.please_enter_javaScript
}
if (networkTimeout <= 0) {
@@ -193,27 +196,34 @@ class RealViewSitePresenter @Inject constructor(
return
}
- val newModel = currentModel!!.copy(
- name = name,
- url = url,
- status = WAITING,
- checkInterval = checkInterval,
+ val updatedSettings = currentModel!!.settings!!.copy(
+ validationIntervalMs = checkInterval,
validationMode = validationMode,
- validationContent = validationContent,
+ validationArgs = validationArgs,
disabled = false,
networkTimeout = networkTimeout
)
+ val updatedModel = currentModel!!.copy(
+ name = name,
+ url = url,
+ settings = updatedSettings
+ )
+ .withStatus(status = WAITING)
with(view!!) {
scopeWhileAttached(Main) {
launch(coroutineContext) {
setLoading()
- async(IO) { serverModelStore.update(newModel) }.await()
+ async(IO) {
+ database.updateSite(updatedModel)
+ }.await()
+
checkStatusManager.scheduleCheck(
- site = newModel,
+ site = updatedModel,
rightNow = true,
cancelPrevious = true
)
+
setDoneLoading()
view?.finish()
}
@@ -222,7 +232,7 @@ class RealViewSitePresenter @Inject constructor(
}
override fun checkNow() = with(view!!) {
- val checkModel = currentModel!!.copy(
+ val checkModel = currentModel!!.withStatus(
status = WAITING
)
view?.displayModel(checkModel)
@@ -242,8 +252,16 @@ class RealViewSitePresenter @Inject constructor(
scopeWhileAttached(Main) {
launch(coroutineContext) {
setLoading()
- currentModel = currentModel!!.copy(disabled = true)
- async(IO) { serverModelStore.update(currentModel!!) }.await()
+ currentModel = currentModel!!.copy(
+ settings = currentModel!!.settings!!.copy(
+ disabled = true
+ )
+ )
+
+ async(IO) {
+ database.updateSite(currentModel!!)
+ }.await()
+
setDoneLoading()
view?.displayModel(currentModel!!)
}
@@ -260,7 +278,9 @@ class RealViewSitePresenter @Inject constructor(
scopeWhileAttached(Main) {
launch(coroutineContext) {
setLoading()
- async(IO) { serverModelStore.delete(site) }.await()
+ async(IO) {
+ database.deleteSite(site)
+ }.await()
setDoneLoading()
view?.finish()
}
@@ -275,7 +295,7 @@ class RealViewSitePresenter @Inject constructor(
currentModel = null
}
- @TestOnly fun setModel(model: ServerModel) {
+ @TestOnly fun setModel(model: Site) {
this.currentModel = model
}
}
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteView.kt b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteView.kt
index 4c79368..7ce7180 100644
--- a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteView.kt
+++ b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteView.kt
@@ -16,7 +16,7 @@
package com.afollestad.nocknock.ui.viewsite
import androidx.annotation.StringRes
-import com.afollestad.nocknock.data.ServerModel
+import com.afollestad.nocknock.data.model.Site
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
import kotlin.coroutines.CoroutineContext
@@ -27,7 +27,7 @@ interface ViewSiteView {
fun setDoneLoading()
- fun displayModel(model: ServerModel)
+ fun displayModel(model: Site)
fun showOrHideUrlSchemeWarning(show: Boolean)
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 0ca585a..3d271c7 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -48,4 +48,12 @@
app:rippleColor="#40ffffff"
/>
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e0273e3..ce75a62 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -22,33 +22,32 @@
Please enter a name!
Please enter a URL.
Please enter a valid URL.
- Please input a check interval.
+ Please input a validation interval.
Please input a search term.
Please input a validation script.
Please enter a network timeout greater than 0.
Options
- Already checking sites!
Remove Site
%1$s from your sites?]]>
Remove
Save Changes
View Site
- Last Check Result
- Next Check
- Next Check: %1$s
+ Last Validation Result
+ Next Validation
+ Next Validation: %1$s
Now
None (turned off)
None
- Disable Automatic Checks
+ Disable Automatic Validation
%1$s? The site will not be validated in the background
- until you re-enable checks for it. You can still manually perform checks by tapping the
+ Disable automatic validation for %1$s? The site will not be checked in the background
+ until you re-enable validation for it. You can still manually perform validation by tapping the
Refresh icon at the top of this page.
]]>
Disable
- Enable Checks & Save Changes
+ Enable Auto Validation & Save Changes
Network Response Timeout
10000
@@ -56,22 +55,22 @@
Refresh Status
- Warning: this app checks for server availability with HTTP requests. It\'s recommended that you
+ Warning: this app validates sites availability with HTTP requests. It\'s recommended that you
use an HTTP URL.
Response Validation Mode
Search term…
- The HTTP status code is checked. If it\'s a successful status code, the site passes the check.
+ The HTTP status code is checked. If it\'s a successful status code, the site passes validation.
The status code check is done first. If it\'s successful, the response body is checked.
- If it contains your search term, the site passes the check.
+ If it contains your search term, the site passes validation.
The status code check is done first. If it\'s successful, the response body is passed to the
- JavaScript function above. If the function returns true, the site passes the check. Throw an
+ JavaScript function above. If the function returns true, the site passes validation. Throw an
exception to pass custom error messages to Nock Nock.
diff --git a/app/src/test/java/com/afollestad/nocknock/AddSitePresenterTest.kt b/app/src/test/java/com/afollestad/nocknock/AddSitePresenterTest.kt
index 20d925c..3c3dd01 100644
--- a/app/src/test/java/com/afollestad/nocknock/AddSitePresenterTest.kt
+++ b/app/src/test/java/com/afollestad/nocknock/AddSitePresenterTest.kt
@@ -15,12 +15,12 @@
*/
package com.afollestad.nocknock
-import com.afollestad.nocknock.data.ServerModel
-import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
-import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
-import com.afollestad.nocknock.engine.db.ServerModelStore
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
+import com.afollestad.nocknock.data.model.Site
+import com.afollestad.nocknock.data.model.SiteSettings
+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.engine.statuscheck.ValidationManager
import com.afollestad.nocknock.ui.addsite.AddSiteView
import com.afollestad.nocknock.ui.addsite.InputErrors
import com.afollestad.nocknock.ui.addsite.RealAddSitePresenter
@@ -42,16 +42,12 @@ import org.junit.Test
class AddSitePresenterTest {
- private val serverModelStore = mock {
- on { runBlocking { put(any()) } } doAnswer { inv ->
- inv.getArgument(0)
- }
- }
- private val checkStatusManager = mock()
+ private val database = mockDatabase()
+ private val checkStatusManager = mock()
private val view = mock()
private val presenter = RealAddSitePresenter(
- serverModelStore,
+ database,
checkStatusManager
)
@@ -260,10 +256,21 @@ class AddSitePresenterTest {
60000
)
- val modelCaptor = argumentCaptor()
+ val siteCaptor = argumentCaptor()
+ val settingsCaptor = argumentCaptor()
+
verify(view).setLoading()
- verify(serverModelStore).put(modelCaptor.capture())
- val model = modelCaptor.firstValue
+ verify(database.siteDao()).insert(siteCaptor.capture())
+ verify(database.siteSettingsDao()).insert(settingsCaptor.capture())
+ verify(database.validationResultsDao(), never()).insert(any())
+
+ val settings = settingsCaptor.firstValue
+ val model = siteCaptor.firstValue.copy(
+ id = 1, // fill it in because our insert captor doesn't catch this
+ settings = settings,
+ lastResult = null
+ )
+
verify(view, never()).setInputErrors(any())
verify(checkStatusManager).scheduleCheck(
site = model,
@@ -271,6 +278,7 @@ class AddSitePresenterTest {
cancelPrevious = true,
fromFinishingJob = false
)
+
verify(view).setDoneLoading()
verify(view).onSiteAdded()
}
diff --git a/app/src/test/java/com/afollestad/nocknock/MainPresenterTest.kt b/app/src/test/java/com/afollestad/nocknock/MainPresenterTest.kt
index 56bc2ff..04135d7 100644
--- a/app/src/test/java/com/afollestad/nocknock/MainPresenterTest.kt
+++ b/app/src/test/java/com/afollestad/nocknock/MainPresenterTest.kt
@@ -15,13 +15,14 @@
*/
package com.afollestad.nocknock
+import android.app.Application
+import android.content.Context.MODE_PRIVATE
import android.content.Intent
-import com.afollestad.nocknock.data.ServerModel
-import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.engine.db.ServerModelStore
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
+import android.content.SharedPreferences
+import com.afollestad.nocknock.data.model.Site
+import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.ACTION_STATUS_UPDATE
+import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.KEY_UPDATE_MODEL
+import com.afollestad.nocknock.engine.statuscheck.ValidationManager
import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.ui.main.MainView
import com.afollestad.nocknock.ui.main.RealMainPresenter
@@ -42,13 +43,22 @@ import org.junit.Test
class MainPresenterTest {
- private val serverModelStore = mock()
+ private val prefs = mock {
+ on { getBoolean("did_db_migration", false) } doReturn true
+ }
+ private val app = mock {
+ on { getSharedPreferences("settings", MODE_PRIVATE) } doReturn prefs
+ }
+
+ private val database = mockDatabase()
+
private val notificationManager = mock()
- private val checkStatusManager = mock()
+ private val checkStatusManager = mock()
private val view = mock()
private val presenter = RealMainPresenter(
- serverModelStore,
+ app,
+ database,
notificationManager,
checkStatusManager
)
@@ -72,55 +82,45 @@ class MainPresenterTest {
val badIntent = fakeIntent("Hello World")
presenter.onBroadcast(badIntent)
- val model = fakeModel()
val goodIntent = fakeIntent(ACTION_STATUS_UPDATE)
whenever(goodIntent.getSerializableExtra(KEY_UPDATE_MODEL))
- .doReturn(model)
+ .doReturn(MOCK_MODEL_2)
presenter.onBroadcast(goodIntent)
- verify(view, times(1)).updateModel(model)
+ verify(view, times(1)).updateModel(MOCK_MODEL_2)
}
@Test fun resume() = runBlocking {
- val model = fakeModel()
- whenever(serverModelStore.get()).doReturn(listOf(model))
presenter.resume()
verify(notificationManager).cancelStatusNotifications()
- val modelsCaptor = argumentCaptor>()
+ val modelsCaptor = argumentCaptor>()
verify(view, times(2)).setModels(modelsCaptor.capture())
assertThat(modelsCaptor.firstValue).isEmpty()
- assertThat(modelsCaptor.lastValue.single()).isEqualTo(model)
+ assertThat(modelsCaptor.lastValue).isEqualTo(ALL_MOCK_MODELS)
}
@Test fun refreshSite() {
- val model = fakeModel()
- presenter.refreshSite(model)
+ presenter.refreshSite(MOCK_MODEL_3)
verify(checkStatusManager).scheduleCheck(
- site = model,
+ site = MOCK_MODEL_3,
rightNow = true,
cancelPrevious = true
)
}
@Test fun removeSite() = runBlocking {
- val model = fakeModel()
- presenter.removeSite(model)
+ presenter.removeSite(MOCK_MODEL_1)
- verify(checkStatusManager).cancelCheck(model)
- verify(notificationManager).cancelStatusNotification(model)
- verify(serverModelStore).delete(model)
- verify(view).onSiteDeleted(model)
+ verify(checkStatusManager).cancelCheck(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(view).onSiteDeleted(MOCK_MODEL_1)
}
- private fun fakeModel() = ServerModel(
- name = "Test",
- url = "https://test.com",
- validationMode = STATUS_CODE
- )
-
private fun fakeIntent(action: String): Intent {
return mock {
on { getAction() } doReturn action
diff --git a/app/src/test/java/com/afollestad/nocknock/TestUtil.kt b/app/src/test/java/com/afollestad/nocknock/TestUtil.kt
new file mode 100644
index 0000000..fe5d524
--- /dev/null
+++ b/app/src/test/java/com/afollestad/nocknock/TestUtil.kt
@@ -0,0 +1,125 @@
+/**
+ * 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 com.afollestad.nocknock.data.AppDatabase
+import com.afollestad.nocknock.data.SiteDao
+import com.afollestad.nocknock.data.SiteSettingsDao
+import com.afollestad.nocknock.data.ValidationResultsDao
+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 fakeSettingsModel(
+ id: Long,
+ validationMode: ValidationMode = STATUS_CODE
+) = SiteSettings(
+ siteId = id,
+ validationIntervalMs = 600000,
+ validationMode = validationMode,
+ validationArgs = null,
+ disabled = false,
+ networkTimeout = 10000
+)
+
+fun fakeResultModel(
+ id: Long,
+ status: Status = OK,
+ reason: String? = null
+) = ValidationResult(
+ siteId = id,
+ status = status,
+ reason = reason,
+ timestampMs = currentTimeMillis()
+)
+
+fun fakeModel(id: Long) = Site(
+ id = id,
+ name = "Test",
+ url = "https://test.com",
+ settings = fakeSettingsModel(id),
+ lastResult = fakeResultModel(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
+ }
+
+ return mock {
+ on { siteDao() } doReturn siteDao
+ on { siteSettingsDao() } doReturn settingsDao
+ on { validationResultsDao() } doReturn resultsDao
+ }
+}
diff --git a/app/src/test/java/com/afollestad/nocknock/ViewSitePresenterTest.kt b/app/src/test/java/com/afollestad/nocknock/ViewSitePresenterTest.kt
index 2a7e1dd..37a78b0 100644
--- a/app/src/test/java/com/afollestad/nocknock/ViewSitePresenterTest.kt
+++ b/app/src/test/java/com/afollestad/nocknock/ViewSitePresenterTest.kt
@@ -16,16 +16,17 @@
package com.afollestad.nocknock
import android.content.Intent
-import com.afollestad.nocknock.data.LAST_CHECK_NONE
-import com.afollestad.nocknock.data.ServerModel
-import com.afollestad.nocknock.data.ServerStatus.WAITING
-import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
-import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
-import com.afollestad.nocknock.engine.db.ServerModelStore
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
+import com.afollestad.nocknock.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.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.statuscheck.ValidationJob.Companion.ACTION_STATUS_UPDATE
+import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.KEY_UPDATE_MODEL
+import com.afollestad.nocknock.engine.statuscheck.ValidationManager
import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.ui.viewsite.InputErrors
import com.afollestad.nocknock.ui.viewsite.KEY_VIEW_MODEL
@@ -47,20 +48,17 @@ import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Test
+import java.lang.System.currentTimeMillis
class ViewSitePresenterTest {
- private val serverModelStore = mock {
- on { runBlocking { put(any()) } } doAnswer { inv ->
- inv.getArgument(0)
- }
- }
- private val checkStatusManager = mock()
+ private val database = mockDatabase()
+ private val checkStatusManager = mock()
private val notificationManager = mock()
private val view = mock()
private val presenter = RealViewSitePresenter(
- serverModelStore,
+ database,
checkStatusManager,
notificationManager
)
@@ -73,13 +71,12 @@ class ViewSitePresenterTest {
}.whenever(view)
.scopeWhileAttached(any(), any())
- val model = fakeModel()
val intent = fakeIntent("")
whenever(intent.getSerializableExtra(KEY_VIEW_MODEL))
- .doReturn(model)
+ .doReturn(MOCK_MODEL_1)
presenter.takeView(view, intent)
- assertThat(presenter.currentModel()).isEqualTo(model)
- verify(view, times(1)).displayModel(model)
+ assertThat(presenter.currentModel()).isEqualTo(MOCK_MODEL_1)
+ verify(view, times(1)).displayModel(MOCK_MODEL_1)
}
@After fun destroy() {
@@ -90,27 +87,25 @@ class ViewSitePresenterTest {
val badIntent = fakeIntent("Hello World")
presenter.onBroadcast(badIntent)
- val model = fakeModel().copy(lastCheck = 0)
val goodIntent = fakeIntent(ACTION_STATUS_UPDATE)
whenever(goodIntent.getSerializableExtra(KEY_UPDATE_MODEL))
- .doReturn(model)
+ .doReturn(MOCK_MODEL_2)
presenter.onBroadcast(goodIntent)
- assertThat(presenter.currentModel()).isEqualTo(model)
- verify(view, times(1)).displayModel(model)
+ assertThat(presenter.currentModel()).isEqualTo(MOCK_MODEL_2)
+ verify(view, times(1)).displayModel(MOCK_MODEL_2)
}
@Test fun onNewIntent() {
val badIntent = fakeIntent(ACTION_STATUS_UPDATE)
presenter.onBroadcast(badIntent)
- val model = fakeModel().copy(lastCheck = 0)
val goodIntent = fakeIntent(ACTION_STATUS_UPDATE)
whenever(goodIntent.getSerializableExtra(KEY_VIEW_MODEL))
- .doReturn(model)
+ .doReturn(MOCK_MODEL_3)
presenter.onBroadcast(goodIntent)
- verify(view, times(1)).displayModel(model)
+ verify(view, times(1)).displayModel(MOCK_MODEL_3)
}
@Test fun onUrlInputFocusChange_focused() {
@@ -298,10 +293,19 @@ class ViewSitePresenterTest {
val url = "https://hello.com"
val checkInterval = 60000L
val validationMode = TERM_SEARCH
- val validationContent = "Hello World"
+ val validationArgs = "Hello World"
- val disabledModel = presenter.currentModel()
- .copy(disabled = true)
+ val currentModel = presenter.currentModel()
+ val initialLastResult = ValidationResult(
+ siteId = currentModel.id,
+ timestampMs = currentTimeMillis() - 60000,
+ status = ERROR,
+ reason = "Oh no!"
+ )
+ val disabledModel = currentModel.copy(
+ settings = currentModel.settings!!.copy(disabled = true),
+ lastResult = initialLastResult
+ )
presenter.setModel(disabledModel)
presenter.commit(
@@ -309,21 +313,38 @@ class ViewSitePresenterTest {
url,
checkInterval,
validationMode,
- validationContent,
+ validationArgs,
60000
)
- val modelCaptor = argumentCaptor()
- verify(view).setLoading()
- verify(serverModelStore).update(modelCaptor.capture())
+ val siteCaptor = argumentCaptor()
+ val settingsCaptor = argumentCaptor()
+ val resultCaptor = argumentCaptor()
- val model = modelCaptor.firstValue
- assertThat(model.name).isEqualTo(name)
- assertThat(model.url).isEqualTo(url)
- assertThat(model.checkInterval).isEqualTo(checkInterval)
- assertThat(model.validationMode).isEqualTo(validationMode)
- assertThat(model.validationContent).isEqualTo(validationContent)
- assertThat(model.disabled).isFalse()
+ verify(view).setLoading()
+ verify(database.siteDao()).update(siteCaptor.capture())
+ verify(database.siteSettingsDao()).update(settingsCaptor.capture())
+ verify(database.validationResultsDao()).update(resultCaptor.capture())
+
+ val model = siteCaptor.firstValue
+ model.apply {
+ assertThat(this.name).isEqualTo(name)
+ assertThat(this.url).isEqualTo(url)
+ }
+
+ val settings = settingsCaptor.firstValue
+ settings.apply {
+ assertThat(this.validationIntervalMs).isEqualTo(checkInterval)
+ assertThat(this.validationArgs).isEqualTo(validationArgs)
+ assertThat(this.disabled).isFalse()
+ }
+
+ val result = resultCaptor.firstValue
+ result.apply {
+ assertThat(this.status).isEqualTo(WAITING)
+ assertThat(this.reason).isNull()
+ assertThat(this.timestampMs).isGreaterThan(0)
+ }
verify(view, never()).setInputErrors(any())
verify(checkStatusManager).scheduleCheck(
@@ -338,9 +359,7 @@ class ViewSitePresenterTest {
@Test fun checkNow() {
val newModel = presenter.currentModel()
- .copy(
- status = WAITING
- )
+ .withStatus(status = WAITING)
presenter.checkNow()
verify(view, never()).setLoading()
@@ -360,11 +379,18 @@ class ViewSitePresenterTest {
verify(notificationManager).cancelStatusNotification(model)
verify(view).setLoading()
- val modelCaptor = argumentCaptor()
- verify(serverModelStore).update(modelCaptor.capture())
+ val modelCaptor = argumentCaptor()
+ val settingsCaptor = argumentCaptor()
+ val resultCaptor = argumentCaptor()
+
+ verify(database.siteDao()).update(modelCaptor.capture())
+ verify(database.siteSettingsDao()).update(settingsCaptor.capture())
+ verify(database.validationResultsDao()).update(resultCaptor.capture())
+
val newModel = modelCaptor.firstValue
- assertThat(newModel.disabled).isTrue()
- assertThat(newModel.lastCheck).isEqualTo(LAST_CHECK_NONE)
+ val newSettings = settingsCaptor.firstValue
+ val result = resultCaptor.firstValue
+ assertThat(newSettings.disabled).isTrue()
verify(view).setDoneLoading()
verify(view, times(1)).displayModel(newModel)
@@ -377,18 +403,15 @@ class ViewSitePresenterTest {
verify(checkStatusManager).cancelCheck(model)
verify(notificationManager).cancelStatusNotification(model)
verify(view).setLoading()
- verify(serverModelStore).delete(model)
+
+ verify(database.siteSettingsDao()).delete(model.settings!!)
+ verify(database.validationResultsDao()).delete(model.lastResult!!)
+ verify(database.siteDao()).delete(model)
+
verify(view).setDoneLoading()
verify(view).finish()
}
- private fun fakeModel() = ServerModel(
- id = 1,
- name = "Test",
- url = "https://test.com",
- validationMode = STATUS_CODE
- )
-
private fun fakeIntent(action: String): Intent {
return mock {
on { getAction() } doReturn action
diff --git a/data/build.gradle b/data/build.gradle
index 4ff907d..6a79c06 100644
--- a/data/build.gradle
+++ b/data/build.gradle
@@ -1,6 +1,7 @@
apply from: '../dependencies.gradle'
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-kapt'
android {
compileSdkVersion versions.compileSdk
@@ -10,6 +11,8 @@ android {
targetSdkVersion versions.compileSdk
versionCode versions.publishVersionCode
versionName versions.publishVersion
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
@@ -17,6 +20,17 @@ dependencies {
implementation project(':utilities')
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin
+
+ implementation 'com.google.dagger:dagger:' + versions.dagger
+ annotationProcessor 'com.google.dagger:dagger-compiler:' + versions.dagger
+
+ 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
new file mode 100644
index 0000000..17a2157
--- /dev/null
+++ b/data/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/data/src/androidTest/java/com/afollestad/nocknock/data/AppDatabaseTest.kt b/data/src/androidTest/java/com/afollestad/nocknock/data/AppDatabaseTest.kt
new file mode 100644
index 0000000..27e222c
--- /dev/null
+++ b/data/src/androidTest/java/com/afollestad/nocknock/data/AppDatabaseTest.kt
@@ -0,0 +1,350 @@
+/**
+ * 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.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
+
+ @Before fun setup() {
+ val context = getApplicationContext()
+ db = inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
+ sitesDao = db.siteDao()
+ settingsDao = db.siteSettingsDao()
+ resultsDao = db.validationResultsDao()
+ }
+
+ @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",
+ settings = null,
+ lastResult = null
+ )
+ val newId1 = sitesDao.insert(model1)
+ assertThat(newId1).isGreaterThan(0)
+
+ val model2 = Site(
+ name = "Test 2",
+ url = "https://test2.com",
+ settings = null,
+ lastResult = null
+ )
+ 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",
+ settings = null,
+ lastResult = null
+ )
+ 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",
+ settings = null,
+ lastResult = null
+ )
+ 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",
+ settings = null,
+ lastResult = null
+ )
+ val newId1 = sitesDao.insert(model1)
+ assertThat(newId1).isGreaterThan(0)
+
+ val model2 = Site(
+ name = "Test 2",
+ url = "https://test2.com",
+ settings = null,
+ lastResult = null
+ )
+ 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
+ )
+ 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
+ )
+ )
+
+ 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
+ )
+ )
+
+ 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()
+ }
+
+ // 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)
+ assertThat(allSites[1]).isEqualTo(MOCK_MODEL_2)
+ assertThat(allSites[2]).isEqualTo(MOCK_MODEL_3)
+ }
+
+ @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 updatedModel = modelToUpdate.copy(
+ name = "Oijrfouhef",
+ url = "https://iojfdfsdk.io",
+ settings = updatedSettings,
+ lastResult = updatedValidationResult
+ )
+
+ db.updateSite(updatedModel)
+
+ val finalSite = db.getSite(modelToUpdate.id)!!
+ assertThat(finalSite.settings).isEqualTo(updatedSettings)
+ assertThat(finalSite.lastResult).isEqualTo(updatedValidationResult)
+ 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(MOCK_MODEL_2)
+
+ 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!!)
+ }
+}
diff --git a/data/src/androidTest/java/com/afollestad/nocknock/data/TestUtil.kt b/data/src/androidTest/java/com/afollestad/nocknock/data/TestUtil.kt
new file mode 100644
index 0000000..7d3ed01
--- /dev/null
+++ b/data/src/androidTest/java/com/afollestad/nocknock/data/TestUtil.kt
@@ -0,0 +1,60 @@
+/**
+ * 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.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
+)
+
+fun fakeResultModel(
+ id: Long,
+ status: Status = OK,
+ reason: String? = null
+) = ValidationResult(
+ siteId = id,
+ status = status,
+ reason = reason,
+ timestampMs = currentTimeMillis()
+)
+
+fun fakeModel(id: Long) = Site(
+ id = id,
+ name = "Test",
+ url = "https://test.com",
+ settings = fakeSettingsModel(id),
+ lastResult = fakeResultModel(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/java/com/afollestad/nocknock/data/AppDatabase.kt b/data/src/main/java/com/afollestad/nocknock/data/AppDatabase.kt
new file mode 100644
index 0000000..8e49e08
--- /dev/null
+++ b/data/src/main/java/com/afollestad/nocknock/data/AppDatabase.kt
@@ -0,0 +1,147 @@
+/**
+ * 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.Site
+import com.afollestad.nocknock.data.model.SiteSettings
+import com.afollestad.nocknock.data.model.ValidationResult
+
+/** @author Aidan Follestad (@afollestad) */
+@Database(
+ entities = [
+ ValidationResult::class,
+ SiteSettings::class,
+ Site::class
+ ],
+ version = 1,
+ exportSchema = false
+)
+@TypeConverters(Converters::class)
+abstract class AppDatabase : RoomDatabase() {
+
+ abstract fun siteDao(): SiteDao
+
+ abstract fun siteSettingsDao(): SiteSettingsDao
+
+ abstract fun validationResultsDao(): ValidationResultsDao
+}
+
+/**
+ * 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()
+ return@map it.copy(
+ settings = settings,
+ lastResult = lastResult
+ )
+ }
+}
+
+/**
+ * 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()
+ return result.copy(
+ settings = settings,
+ lastResult = lastResult
+ )
+}
+
+/**
+ * Inserts a site along with its settings and last result into the database.
+ *
+ * @author Aidan Follestad (@afollestad)
+ */
+fun AppDatabase.putSite(site: Site): Site {
+ requireNotNull(site.settings) { "Settings must be populated." }
+ val newId = siteDao().insert(site)
+ val settingsWithSiteId =
+ site.settings!!.copy(
+ siteId = newId
+ )
+ siteSettingsDao().insert(settingsWithSiteId)
+ site.lastResult?.let { validationResultsDao().insert(it) }
+ return site.copy(
+ id = newId,
+ settings = settingsWithSiteId
+ )
+}
+
+/**
+ * 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)
+ if (site.settings != null) {
+ val existing = siteSettingsDao().forSite(site.id)
+ .singleOrNull()
+ if (existing != null) {
+ siteSettingsDao().update(site.settings!!)
+ } else {
+ siteSettingsDao().insert(
+ site.settings!!.copy(
+ siteId = site.id
+ )
+ )
+ }
+ }
+ if (site.lastResult != null) {
+ val existing = validationResultsDao().forSite(site.id)
+ .singleOrNull()
+ if (existing != null) {
+ validationResultsDao().update(site.lastResult!!)
+ } else {
+ validationResultsDao().insert(site.lastResult!!)
+ }
+ }
+}
+
+/**
+ * Deletes a site along with its settings and last result from the database.
+ *
+ * @author Aidan Follestad (@afollestad)
+ */
+fun AppDatabase.deleteSite(site: Site) {
+ if (site.settings != null) {
+ siteSettingsDao().delete(site.settings!!)
+ }
+ if (site.lastResult != null) {
+ validationResultsDao().delete(site.lastResult!!)
+ }
+ siteDao().delete(site)
+}
diff --git a/data/src/main/java/com/afollestad/nocknock/data/ServerStatus.kt b/data/src/main/java/com/afollestad/nocknock/data/ServerStatus.kt
deleted file mode 100644
index 9cc28d0..0000000
--- a/data/src/main/java/com/afollestad/nocknock/data/ServerStatus.kt
+++ /dev/null
@@ -1,50 +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.ServerStatus.CHECKING
-import com.afollestad.nocknock.data.ServerStatus.OK
-import com.afollestad.nocknock.data.ServerStatus.WAITING
-
-/** @author Aidan Follestad (@afollestad) */
-enum class ServerStatus(val value: Int) {
- OK(1),
- WAITING(2),
- CHECKING(3),
- ERROR(4);
-
- companion object {
-
- fun fromValue(value: Int) = when (value) {
- OK.value -> OK
- WAITING.value -> WAITING
- CHECKING.value -> CHECKING
- ERROR.value -> ERROR
- else -> throw IllegalArgumentException("Unknown validationMode: $value")
- }
- }
-}
-
-fun ServerStatus.textRes() = when (this) {
- OK -> R.string.everything_checks_out
- WAITING -> R.string.waiting
- CHECKING -> R.string.checking_status
- else -> 0
-}
-
-fun Int.toServerStatus() = ServerStatus.fromValue(this)
-
-fun ServerStatus.isPending() = this == WAITING || this == CHECKING
diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/EngineModule.kt b/data/src/main/java/com/afollestad/nocknock/data/SiteDao.kt
similarity index 50%
rename from engine/src/main/java/com/afollestad/nocknock/engine/EngineModule.kt
rename to data/src/main/java/com/afollestad/nocknock/data/SiteDao.kt
index b9fd1e1..135a8f1 100644
--- a/engine/src/main/java/com/afollestad/nocknock/engine/EngineModule.kt
+++ b/data/src/main/java/com/afollestad/nocknock/data/SiteDao.kt
@@ -13,29 +13,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.afollestad.nocknock.engine
+package com.afollestad.nocknock.data
-import com.afollestad.nocknock.engine.db.RealServerModelStore
-import com.afollestad.nocknock.engine.db.ServerModelStore
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
-import com.afollestad.nocknock.engine.statuscheck.RealCheckStatusManager
-import dagger.Binds
-import dagger.Module
-import javax.inject.Singleton
+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) */
-@Module
-abstract class EngineModule {
+@Dao
+interface SiteDao {
- @Binds
- @Singleton
- abstract fun provideServerModelStore(
- serverModelStore: RealServerModelStore
- ): ServerModelStore
+ @Query("SELECT * FROM sites ORDER BY name ASC")
+ fun all(): List
- @Binds
- @Singleton
- abstract fun provideCheckStatusManager(
- checkStatusManager: RealCheckStatusManager
- ): CheckStatusManager
+ @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
new file mode 100644
index 0000000..b11f418
--- /dev/null
+++ b/data/src/main/java/com/afollestad/nocknock/data/SiteSettingsDao.kt
@@ -0,0 +1,44 @@
+/**
+ * Designed and developed by Aidan Follestad (@afollestad)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.afollestad.nocknock.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
new file mode 100644
index 0000000..d6c81dc
--- /dev/null
+++ b/data/src/main/java/com/afollestad/nocknock/data/ValidationResultsDao.kt
@@ -0,0 +1,44 @@
+/**
+ * Designed and developed by Aidan Follestad (@afollestad)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.afollestad.nocknock.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/legacy/DbMigrator.kt b/data/src/main/java/com/afollestad/nocknock/data/legacy/DbMigrator.kt
new file mode 100644
index 0000000..5af752b
--- /dev/null
+++ b/data/src/main/java/com/afollestad/nocknock/data/legacy/DbMigrator.kt
@@ -0,0 +1,105 @@
+/**
+ * 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("DEPRECATION")
+
+package com.afollestad.nocknock.data.legacy
+
+import android.app.Application
+import com.afollestad.nocknock.data.AppDatabase
+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.WAITING
+import com.afollestad.nocknock.data.model.ValidationResult
+import javax.inject.Inject
+
+/**
+ * Migrates from manual SQLite management to Room.
+ *
+ * @author Aidan Follestad (@afollestad)
+ */
+class DbMigrator @Inject constructor(
+ app: Application,
+ private val appDb: AppDatabase
+) {
+ private val legacyStore = ServerModelStore(app)
+
+ fun migrateAll(): Int {
+ val legacyModels = legacyStore.get()
+ var count = 0
+
+ for (oldModel in legacyModels) {
+ // Insert site
+ val site = oldModel.toNewModel()
+ val siteId = appDb.siteDao()
+ .insert(site)
+
+ // Insert site settings
+ val settingsWithId = site.settings!!.copy(
+ siteId = siteId
+ )
+ appDb.siteSettingsDao()
+ .insert(settingsWithId)
+
+ // Insert validation result
+ site.lastResult?.let {
+ val resultWithId = it.copy(
+ siteId = siteId
+ )
+ appDb.validationResultsDao()
+ .insert(resultWithId)
+ }
+
+ count++
+ }
+
+ legacyStore.wipe()
+ return count
+ }
+
+ private fun ServerModel.toNewModel(): Site {
+ return Site(
+ id = 0,
+ name = this.name,
+ url = this.url,
+ settings = this.toSettingsModel(),
+ lastResult = this.toValidationModel()
+ )
+ }
+
+ private fun ServerModel.toSettingsModel(): SiteSettings {
+ return SiteSettings(
+ siteId = 0,
+ validationIntervalMs = this.checkInterval,
+ validationMode = this.validationMode,
+ validationArgs = this.validationContent,
+ disabled = this.disabled,
+ networkTimeout = this.networkTimeout
+ )
+ }
+
+ private fun ServerModel.toValidationModel(): ValidationResult? {
+ if (this.lastCheck == LAST_CHECK_NONE) {
+ return null
+ }
+ return ValidationResult(
+ siteId = 0,
+ timestampMs = this.lastCheck,
+ status = if (this.status == CHECKING) WAITING else this.status,
+ reason = this.reason
+ )
+ }
+}
diff --git a/data/src/main/java/com/afollestad/nocknock/data/ServerModel.kt b/data/src/main/java/com/afollestad/nocknock/data/legacy/ServerModel.kt
similarity index 55%
rename from data/src/main/java/com/afollestad/nocknock/data/ServerModel.kt
rename to data/src/main/java/com/afollestad/nocknock/data/legacy/ServerModel.kt
index 474e514..9882705 100644
--- a/data/src/main/java/com/afollestad/nocknock/data/ServerModel.kt
+++ b/data/src/main/java/com/afollestad/nocknock/data/legacy/ServerModel.kt
@@ -13,25 +13,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.afollestad.nocknock.data
+@file:Suppress("DEPRECATION")
+
+package com.afollestad.nocknock.data.legacy
import android.content.ContentValues
import android.database.Cursor
-import com.afollestad.nocknock.data.ServerStatus.OK
-import com.afollestad.nocknock.utilities.ext.timeString
-import com.afollestad.nocknock.utilities.providers.IdProvider
-import java.lang.System.currentTimeMillis
-import kotlin.math.max
+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.toSiteStatus
+import com.afollestad.nocknock.data.model.toValidationMode
const val CHECK_INTERVAL_UNSET = -1L
const val LAST_CHECK_NONE = -1L
/** @author Aidan Follestad (@afollestad)*/
+@Deprecated("Deprecated in favor of Site.")
data class ServerModel(
var id: Int = 0,
val name: String,
val url: String,
- val status: ServerStatus = OK,
+ val status: Status = OK,
val checkInterval: Long = CHECK_INTERVAL_UNSET,
val lastCheck: Long = LAST_CHECK_NONE,
val reason: String? = null,
@@ -39,7 +42,7 @@ data class ServerModel(
val validationContent: String? = null,
val disabled: Boolean = false,
val networkTimeout: Int = 0
-) : IdProvider {
+) {
companion object {
const val TABLE_NAME = "server_models"
@@ -59,31 +62,65 @@ data class ServerModel(
fun pull(cursor: Cursor): ServerModel {
return ServerModel(
- id = cursor.getInt(cursor.getColumnIndex(COLUMN_ID)),
- name = cursor.getString(cursor.getColumnIndex(COLUMN_NAME)),
- url = cursor.getString(cursor.getColumnIndex(COLUMN_URL)),
- status = cursor.getInt(cursor.getColumnIndex(COLUMN_STATUS)).toServerStatus(),
- checkInterval = cursor.getLong(cursor.getColumnIndex(COLUMN_CHECK_INTERVAL)),
- lastCheck = cursor.getLong(cursor.getColumnIndex(COLUMN_LAST_CHECK)),
- reason = cursor.getString(cursor.getColumnIndex(COLUMN_REASON)),
+ id = cursor.getInt(
+ cursor.getColumnIndex(
+ COLUMN_ID
+ )
+ ),
+ name = cursor.getString(
+ cursor.getColumnIndex(
+ COLUMN_NAME
+ )
+ ),
+ url = cursor.getString(
+ cursor.getColumnIndex(
+ COLUMN_URL
+ )
+ ),
+ status = cursor.getInt(
+ cursor.getColumnIndex(
+ COLUMN_STATUS
+ )
+ ).toSiteStatus(),
+ checkInterval = cursor.getLong(
+ cursor.getColumnIndex(
+ COLUMN_CHECK_INTERVAL
+ )
+ ),
+ lastCheck = cursor.getLong(
+ cursor.getColumnIndex(
+ COLUMN_LAST_CHECK
+ )
+ ),
+ reason = cursor.getString(
+ cursor.getColumnIndex(
+ COLUMN_REASON
+ )
+ ),
validationMode = cursor.getInt(
- cursor.getColumnIndex(COLUMN_VALIDATION_MODE)
+ cursor.getColumnIndex(
+ COLUMN_VALIDATION_MODE
+ )
).toValidationMode(),
- validationContent = cursor.getString(cursor.getColumnIndex(COLUMN_VALIDATION_CONTENT)),
- disabled = cursor.getInt(cursor.getColumnIndex(COLUMN_DISABLED)) == 1,
- networkTimeout = cursor.getInt(cursor.getColumnIndex(COLUMN_NETWORK_TIMEOUT))
+ validationContent = cursor.getString(
+ cursor.getColumnIndex(
+ COLUMN_VALIDATION_CONTENT
+ )
+ ),
+ disabled = cursor.getInt(
+ cursor.getColumnIndex(
+ COLUMN_DISABLED
+ )
+ ) == 1,
+ networkTimeout = cursor.getInt(
+ cursor.getColumnIndex(
+ COLUMN_NETWORK_TIMEOUT
+ )
+ )
)
}
}
- override fun id() = id
-
- fun intervalText(): String {
- val now = currentTimeMillis()
- val nextCheck = max(lastCheck, 0) + checkInterval
- return (nextCheck - now).timeString()
- }
-
fun toContentValues() = ContentValues().apply {
put(COLUMN_NAME, name)
put(COLUMN_URL, url)
diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/db/ServerModelDbHelper.kt b/data/src/main/java/com/afollestad/nocknock/data/legacy/ServerModelDbHelper.kt
similarity index 90%
rename from engine/src/main/java/com/afollestad/nocknock/engine/db/ServerModelDbHelper.kt
rename to data/src/main/java/com/afollestad/nocknock/data/legacy/ServerModelDbHelper.kt
index 2fc9a4d..f0bdb2f 100644
--- a/engine/src/main/java/com/afollestad/nocknock/engine/db/ServerModelDbHelper.kt
+++ b/data/src/main/java/com/afollestad/nocknock/data/legacy/ServerModelDbHelper.kt
@@ -13,12 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.afollestad.nocknock.engine.db
+@file:Suppress("DEPRECATION")
+
+package com.afollestad.nocknock.data.legacy
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
-import com.afollestad.nocknock.data.ServerModel
private const val SQL_CREATE_ENTRIES =
"CREATE TABLE ${ServerModel.TABLE_NAME} (" +
@@ -38,8 +39,10 @@ private const val SQL_CREATE_ENTRIES =
private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${ServerModel.TABLE_NAME}"
/** @author Aidan Follestad (@afollestad) */
-class ServerModelDbHelper(context: Context) : SQLiteOpenHelper(
- context, DATABASE_NAME, null, DATABASE_VERSION
+@Deprecated("Use AppDatabase.")
+internal class ServerModelDbHelper(context: Context) : SQLiteOpenHelper(
+ context, DATABASE_NAME, null,
+ DATABASE_VERSION
) {
companion object {
const val DATABASE_VERSION = 3
@@ -71,6 +74,5 @@ class ServerModelDbHelper(context: Context) : SQLiteOpenHelper(
fun wipe() {
this.writableDatabase.execSQL(SQL_DELETE_ENTRIES)
- onCreate(this.writableDatabase)
}
}
diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/db/ServerModelStore.kt b/data/src/main/java/com/afollestad/nocknock/data/legacy/ServerModelStore.kt
similarity index 60%
rename from engine/src/main/java/com/afollestad/nocknock/engine/db/ServerModelStore.kt
rename to data/src/main/java/com/afollestad/nocknock/data/legacy/ServerModelStore.kt
index 9c7ecce..2d0a0cb 100644
--- a/engine/src/main/java/com/afollestad/nocknock/engine/db/ServerModelStore.kt
+++ b/data/src/main/java/com/afollestad/nocknock/data/legacy/ServerModelStore.kt
@@ -13,42 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.afollestad.nocknock.engine.db
+@file:Suppress("DEPRECATION")
+
+package com.afollestad.nocknock.data.legacy
import android.app.Application
+import android.content.ContentValues
import android.database.Cursor
-import com.afollestad.nocknock.data.ServerModel
-import com.afollestad.nocknock.data.ServerModel.Companion.COLUMN_ID
-import com.afollestad.nocknock.data.ServerModel.Companion.DEFAULT_SORT_ORDER
-import com.afollestad.nocknock.data.ServerModel.Companion.TABLE_NAME
-import com.afollestad.nocknock.utilities.ext.diffFrom
-import org.jetbrains.annotations.TestOnly
-import javax.inject.Inject
-import timber.log.Timber.d as log
-import timber.log.Timber.w as warn
+import com.afollestad.nocknock.data.legacy.ServerModel.Companion.COLUMN_ID
+import com.afollestad.nocknock.data.legacy.ServerModel.Companion.DEFAULT_SORT_ORDER
+import com.afollestad.nocknock.data.legacy.ServerModel.Companion.TABLE_NAME
/** @author Aidan Follestad (@afollestad) */
-interface ServerModelStore {
-
- suspend fun get(id: Int? = null): List
-
- suspend fun put(model: ServerModel): ServerModel
-
- suspend fun update(model: ServerModel): Int
-
- suspend fun delete(model: ServerModel): Int
-
- suspend fun delete(id: Int): Int
-
- suspend fun deleteAll(): Int
-}
-
-/** @author Aidan Follestad (@afollestad) */
-class RealServerModelStore @Inject constructor(app: Application) : ServerModelStore {
+@Deprecated("Deprecated in favor of AppDatabase.")
+internal class ServerModelStore(app: Application) {
private val dbHelper = ServerModelDbHelper(app)
- override suspend fun get(id: Int?): List {
+ fun get(id: Int? = null): List {
if (id == null) {
return getAll()
}
@@ -88,19 +70,16 @@ class RealServerModelStore @Inject constructor(app: Application) : ServerModelSt
cursor.use { return readModels(it) }
}
- override suspend fun put(model: ServerModel): ServerModel {
+ fun put(model: ServerModel): ServerModel {
check(model.id == 0) { "Cannot put a model that already has an ID." }
val writer = dbHelper.writableDatabase
val newId = writer.insert(TABLE_NAME, null, model.toContentValues())
return model.copy(id = newId.toInt())
- .apply {
- log("Inserted new site model: $this")
- }
}
- override suspend fun update(model: ServerModel): Int {
+ fun update(model: ServerModel): Int {
check(model.id != 0) { "Cannot update a model that does not have an ID." }
val oldModel = get(model.id).single()
@@ -111,35 +90,27 @@ class RealServerModelStore @Inject constructor(app: Application) : ServerModelSt
val valuesDiff = oldValues.diffFrom(newValues)
if (valuesDiff.size() == 0) {
- warn("Nothing has changed - nothing to update!")
return 0
}
val selection = "$COLUMN_ID = ?"
val selectionArgs = arrayOf("${model.id}")
- log("Updated model: $model")
return writer.update(TABLE_NAME, valuesDiff, selection, selectionArgs)
}
- override suspend fun delete(model: ServerModel) = delete(model.id)
+ fun delete(model: ServerModel) = delete(model.id)
- override suspend fun delete(id: Int): Int {
+ fun delete(id: Int): Int {
check(id != 0) { "Cannot delete a model that doesn't have an ID." }
val selection = "$COLUMN_ID = ?"
val selectionArgs = arrayOf("$id")
- log("Deleted model: $id")
return dbHelper.writableDatabase.delete(TABLE_NAME, selection, selectionArgs)
}
- override suspend fun deleteAll(): Int {
- log("Deleted all models")
- return dbHelper.writableDatabase.delete(TABLE_NAME, null, null)
- }
-
- @TestOnly fun db() = dbHelper
+ fun wipe() = dbHelper.wipe()
private fun readModels(cursor: Cursor): List {
val results = mutableListOf()
@@ -148,4 +119,45 @@ class RealServerModelStore @Inject constructor(app: Application) : ServerModelSt
}
return results
}
+
+ /**
+ * Returns a [ContentValues] instance which contains only values that have changed between
+ * the receiver (original) and parameter (new) instances.
+ */
+ private fun ContentValues.diffFrom(contentValues: ContentValues): ContentValues {
+ val diff = ContentValues()
+ for ((name, oldValue) in this.valueSet()) {
+ val newValue = contentValues.get(name)
+ if (newValue != oldValue) {
+ diff.putAny(name, newValue)
+ }
+ }
+ return diff
+ }
+
+ /**
+ * Auto casts an [Any] value and uses the appropriate `put` method to store it
+ * in the [ContentValues] instance.
+ */
+ private fun ContentValues.putAny(
+ name: String,
+ value: Any?
+ ) {
+ if (value == null) {
+ putNull(name)
+ return
+ }
+ when (value) {
+ is String -> put(name, value)
+ is Byte -> put(name, value)
+ is Short -> put(name, value)
+ is Int -> put(name, value)
+ is Long -> put(name, value)
+ is Float -> put(name, value)
+ is Double -> put(name, value)
+ is Boolean -> put(name, value)
+ is ByteArray -> put(name, value)
+ else -> throw IllegalArgumentException("ContentValues can't hold $value")
+ }
+ }
}
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
new file mode 100644
index 0000000..b532829
--- /dev/null
+++ b/data/src/main/java/com/afollestad/nocknock/data/model/Converters.kt
@@ -0,0 +1,42 @@
+/**
+ * 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/Site.kt b/data/src/main/java/com/afollestad/nocknock/data/model/Site.kt
new file mode 100644
index 0000000..b0539d7
--- /dev/null
+++ b/data/src/main/java/com/afollestad/nocknock/data/model/Site.kt
@@ -0,0 +1,76 @@
+/**
+ * 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,
+ /** Settings for the site. */
+ @Ignore var settings: SiteSettings?,
+ /** The last validation attempt result for the site, if any. */
+ @Ignore var lastResult: ValidationResult?
+) : CanNotifyModel {
+
+ constructor() : this(0, "", "", null, null)
+
+ override fun notiId(): Int = id.toInt()
+
+ override fun notiName(): String = name
+
+ override fun notiTag(): String = url
+
+ 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
new file mode 100644
index 0000000..91667a7
--- /dev/null
+++ b/data/src/main/java/com/afollestad/nocknock/data/model/SiteSettings.kt
@@ -0,0 +1,47 @@
+/**
+ * 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
+) : Serializable {
+
+ constructor() : this(0, 0, STATUS_CODE, null, false, 0)
+}
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
new file mode 100644
index 0000000..4dcc3a0
--- /dev/null
+++ b/data/src/main/java/com/afollestad/nocknock/data/model/Status.kt
@@ -0,0 +1,59 @@
+/**
+ * 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
+
+fun Int.toSiteStatus() = Status.fromValue(this)
diff --git a/data/src/main/java/com/afollestad/nocknock/data/ValidationMode.kt b/data/src/main/java/com/afollestad/nocknock/data/model/ValidationMode.kt
similarity index 66%
rename from data/src/main/java/com/afollestad/nocknock/data/ValidationMode.kt
rename to data/src/main/java/com/afollestad/nocknock/data/model/ValidationMode.kt
index b2150ff..7a461b0 100644
--- a/data/src/main/java/com/afollestad/nocknock/data/ValidationMode.kt
+++ b/data/src/main/java/com/afollestad/nocknock/data/model/ValidationMode.kt
@@ -13,12 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.afollestad.nocknock.data
+package com.afollestad.nocknock.data.model
-/** @author Aidan Follestad (@afollestad) */
+/**
+ * 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);
companion object {
@@ -39,6 +47,8 @@ enum class ValidationMode(val value: Int) {
}
}
-fun Int.toValidationMode() = ValidationMode.fromValue(this)
+fun Int.toValidationMode() =
+ ValidationMode.fromValue(this)
-fun Int.indexToValidationMode() = ValidationMode.fromIndex(this)
+fun Int.indexToValidationMode() =
+ ValidationMode.fromIndex(this)
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
new file mode 100644
index 0000000..3b91214
--- /dev/null
+++ b/data/src/main/java/com/afollestad/nocknock/data/model/ValidationResult.kt
@@ -0,0 +1,43 @@
+/**
+ * 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/dependencies.gradle b/dependencies.gradle
index 2879f24..66a58f3 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -15,7 +15,9 @@ ext.versions = [
dagger : '2.19',
kotlin : '1.3.10',
coroutines : '1.0.1',
+
androidx : '1.0.0',
+ room : '2.0.0',
rxBinding : '3.0.0-alpha1',
@@ -23,9 +25,10 @@ ext.versions = [
rxkPrefs : '1.2.0',
timber : '4.7.1',
- testRunner : '1.0.2',
junit : '4.12',
mockito : '2.23.0',
mockitoKotlin : '2.0.0-RC1',
- truth : '0.42'
+ truth : '0.42',
+ androidxTestRunner: '1.1.0',
+ androidxTest : '1.0.0',
]
diff --git a/engine/build.gradle b/engine/build.gradle
index 7267354..c26b99b 100644
--- a/engine/build.gradle
+++ b/engine/build.gradle
@@ -10,8 +10,6 @@ android {
targetSdkVersion versions.compileSdk
versionCode versions.publishVersionCode
versionName versions.publishVersion
-
- testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}
@@ -34,9 +32,6 @@ dependencies {
testImplementation 'org.mockito:mockito-core:' + versions.mockito
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:' + versions.mockitoKotlin
testImplementation 'com.google.truth:truth:' + versions.truth
-
- androidTestImplementation 'com.android.support.test:runner:' + versions.testRunner
- androidTestImplementation 'com.google.truth:truth:' + versions.truth
}
apply from: '../spotless.gradle'
\ No newline at end of file
diff --git a/engine/src/androidTest/java/com/afollestad/nocknock/engine/ServerModelStoreTest.kt b/engine/src/androidTest/java/com/afollestad/nocknock/engine/ServerModelStoreTest.kt
deleted file mode 100644
index 0563db7..0000000
--- a/engine/src/androidTest/java/com/afollestad/nocknock/engine/ServerModelStoreTest.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.engine
-
-import android.app.Application
-import android.support.test.runner.AndroidJUnit4
-import com.afollestad.nocknock.data.ServerModel
-import com.afollestad.nocknock.data.ServerStatus.CHECKING
-import com.afollestad.nocknock.data.ServerStatus.ERROR
-import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
-import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
-import com.afollestad.nocknock.engine.db.RealServerModelStore
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import android.support.test.InstrumentationRegistry.getTargetContext as context
-
-@RunWith(AndroidJUnit4::class)
-class ServerModelStoreTest {
-
- private lateinit var store: RealServerModelStore
-
- @Before fun setup() {
- store = RealServerModelStore(context().applicationContext as Application)
- store.db()
- .wipe()
- }
-
- @Test fun get() = runBlocking {
- // Put some fake data to retrieve
- store.put(fakeModel(1))
- val model2 = store.put(fakeModel(2))
-
- val model = store.get(2)
- .single()
- assertThat(model).isEqualTo(model2.copy(id = 2))
- }
-
- @Test fun getAll() = runBlocking {
- // Put some fake data to retrieve
- val model1 = store.put(fakeModel(1))
- val model2 = store.put(fakeModel(2))
-
- val models = store.get()
- assertThat(models.size).isEqualTo(2)
- assertThat(models[0]).isEqualTo(model1.copy(id = 1))
- assertThat(models[1]).isEqualTo(model2.copy(id = 2))
- }
-
- @Test fun update() = runBlocking {
- store.put(
- ServerModel(
- name = "Wakanda Forever",
- url = "https://www.wakanda.gov",
- status = ERROR,
- checkInterval = 5,
- lastCheck = 10,
- reason = "Body doesn't contain your term.",
- validationMode = TERM_SEARCH,
- validationContent = "Vibranium",
- disabled = false
- )
- )
- store.put(fakeModel(2))
-
- val originalModel1 = store.get(id = 1)
- .single()
-
- val defaultJs = "var responseObj = JSON.parse(response);\\nreturn responseObj.success === true;"
- val newModel1 = originalModel1.copy(
- name = "HYDRA",
- url = "https://www.hyrda.dict",
- status = CHECKING,
- checkInterval = 10,
- lastCheck = 20,
- reason = "Evaluation failed.",
- validationMode = JAVASCRIPT,
- validationContent = defaultJs,
- disabled = true
- )
- assertThat(store.update(newModel1)).isEqualTo(1)
-
- val newModels = store.get()
- assertThat(newModels.size).isEqualTo(2)
- assertThat(newModels.first()).isEqualTo(newModel1)
- }
-
- @Test fun delete() = runBlocking {
- // Put some fake data to delete
- val model1 = store.put(fakeModel(1))
- val model2 = store.put(fakeModel(2))
-
- assertThat(store.delete(model1)).isEqualTo(1)
-
- val newModels = store.get()
- assertThat(newModels.single()).isEqualTo(model2)
- }
-
- @Test fun deleteAll() = runBlocking {
- // Put some fake data to delete
- store.put(fakeModel(1))
- store.put(fakeModel(2))
-
- store.deleteAll()
- assertThat(store.get()).isEmpty()
- }
-
- private fun fakeModel(index: Int) = ServerModel(
- name = "Model $index",
- url = "https://hello.com/$index",
- validationMode = STATUS_CODE
- )
-}
diff --git a/app/src/main/java/com/afollestad/nocknock/di/MainModule.kt b/engine/src/main/java/com/afollestad/nocknock/engine/EngineModule.java
similarity index 55%
rename from app/src/main/java/com/afollestad/nocknock/di/MainModule.kt
rename to engine/src/main/java/com/afollestad/nocknock/engine/EngineModule.java
index 4054fe4..3df7106 100644
--- a/app/src/main/java/com/afollestad/nocknock/di/MainModule.kt
+++ b/engine/src/main/java/com/afollestad/nocknock/engine/EngineModule.java
@@ -1,4 +1,4 @@
-/**
+/*
* Designed and developed by Aidan Follestad (@afollestad)
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,27 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.afollestad.nocknock.di
+package com.afollestad.nocknock.engine;
-import com.afollestad.nocknock.R
-import com.afollestad.nocknock.ui.main.MainActivity
-import com.afollestad.nocknock.utilities.qualifiers.AppIconRes
-import com.afollestad.nocknock.utilities.qualifiers.MainActivityClass
-import dagger.Module
-import dagger.Provides
-import javax.inject.Singleton
+import com.afollestad.nocknock.engine.statuscheck.RealValidationManager;
+import com.afollestad.nocknock.engine.statuscheck.ValidationManager;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Singleton;
/** @author Aidan Follestad (@afollestad) */
@Module
-open class MainModule {
+public abstract class EngineModule {
- @Provides
+ @Binds
@Singleton
- @AppIconRes
- fun provideAppIconRes(): Int = R.mipmap.ic_launcher
-
- @Provides
- @Singleton
- @MainActivityClass
- fun provideMainActivityClass(): Class<*> = MainActivity::class.java
+ abstract ValidationManager provideCheckStatusManager(RealValidationManager checkStatusManager);
}
diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/BootReceiver.kt b/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/BootReceiver.kt
index 1095220..298d975 100644
--- a/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/BootReceiver.kt
+++ b/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/BootReceiver.kt
@@ -31,7 +31,7 @@ import timber.log.Timber.d as log
/** @author Aidan Follestad (@afollestad) */
class BootReceiver : BroadcastReceiver() {
- @Inject lateinit var checkStatusManager: CheckStatusManager
+ @Inject lateinit var checkStatusManager: ValidationManager
override fun onReceive(
context: Context,
diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusJob.kt b/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/ValidationJob.kt
similarity index 59%
rename from engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusJob.kt
rename to engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/ValidationJob.kt
index efa092d..307db18 100644
--- a/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusJob.kt
+++ b/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/ValidationJob.kt
@@ -18,17 +18,20 @@ package com.afollestad.nocknock.engine.statuscheck
import android.app.job.JobParameters
import android.app.job.JobService
import android.content.Intent
-import com.afollestad.nocknock.data.ServerModel
-import com.afollestad.nocknock.data.ServerStatus
-import com.afollestad.nocknock.data.ServerStatus.CHECKING
-import com.afollestad.nocknock.data.ServerStatus.ERROR
-import com.afollestad.nocknock.data.ServerStatus.OK
-import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
-import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
-import com.afollestad.nocknock.data.isPending
+import com.afollestad.nocknock.data.AppDatabase
+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.getSite
+import com.afollestad.nocknock.data.updateSite
import com.afollestad.nocknock.engine.BuildConfig.APPLICATION_ID
-import com.afollestad.nocknock.engine.db.ServerModelStore
import com.afollestad.nocknock.notifications.NockNotificationManager
import com.afollestad.nocknock.utilities.ext.injector
import com.afollestad.nocknock.utilities.js.JavaScript
@@ -42,8 +45,12 @@ import java.lang.System.currentTimeMillis
import javax.inject.Inject
import timber.log.Timber.d as log
-/** @author Aidan Follestad (@afollestad)*/
-class CheckStatusJob : JobService() {
+/**
+ * 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"
@@ -52,47 +59,50 @@ class CheckStatusJob : JobService() {
const val KEY_SITE_ID = "site.id"
}
- @Inject lateinit var modelStore: ServerModelStore
- @Inject lateinit var checkStatusManager: CheckStatusManager
+ @Inject lateinit var database: AppDatabase
+ @Inject lateinit var checkStatusManager: ValidationManager
@Inject lateinit var notificationManager: NockNotificationManager
override fun onStartJob(params: JobParameters): Boolean {
injector().injectInto(this)
- val siteId = params.extras.getInt(KEY_SITE_ID)
+ val siteId = params.extras.getLong(KEY_SITE_ID)
GlobalScope.launch(Main) {
- val sites = async(IO) { modelStore.get(id = siteId) }.await()
- if (sites.isEmpty()) {
- log("Unable to find any sites for ID $siteId, this job will not be rescheduled.")
+ val site = async(IO) { database.getSite(siteId) }.await()
+ if (site == null) {
+ log("Unable to find a site for ID $siteId, this job will not be rescheduled.")
return@launch jobFinished(params, false)
}
- val site = sites.single()
+ 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 result = async(IO) {
+ val jobResult = async(IO) {
updateStatus(site, CHECKING)
val checkResult = checkStatusManager.performCheck(site)
val resultModel = checkResult.model
val resultResponse = checkResult.response
+ val result = resultModel.lastResult!!
- if (resultModel.status != OK) {
- log("Got unsuccessful check status back: ${resultModel.reason}")
+ if (result.status != OK) {
+ log("Got unsuccessful check status back: ${result.reason}")
return@async updateStatus(site = resultModel)
} else {
- when (site.validationMode) {
+ 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(site.validationContent ?: "")) {
+ return@async if (!body.contains(siteSettings.validationArgs ?: "")) {
updateStatus(
- resultModel.copy(
+ resultModel.withStatus(
status = ERROR,
- reason = "Term \"${site.validationContent}\" not found in response body."
+ reason = "Term \"${siteSettings.validationArgs}\" not found in response body."
)
)
} else {
@@ -102,9 +112,9 @@ class CheckStatusJob : JobService() {
JAVASCRIPT -> {
val body = resultResponse?.body()?.string() ?: ""
log("Using JAVASCRIPT validation mode on body of length: ${body.length}")
- val reason = JavaScript.eval(resultModel.validationContent ?: "", body)
+ val reason = JavaScript.eval(siteSettings.validationArgs ?: "", body)
return@async if (reason != null) {
- updateStatus(resultModel.copy(reason = reason), status = ERROR)
+ updateStatus(resultModel.withStatus(reason = reason), status = ERROR)
} else {
resultModel
}
@@ -113,27 +123,29 @@ class CheckStatusJob : JobService() {
// 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.copy(
+ resultModel.withStatus(
status = OK,
reason = null
)
)
}
else -> {
- throw IllegalArgumentException("Unknown validation mode: ${site.validationMode}")
+ throw IllegalArgumentException(
+ "Unknown validation mode: ${siteSettings.validationArgs}"
+ )
}
}
}
}.await()
- if (result.status == OK) {
- notificationManager.cancelStatusNotification(result)
+ if (jobResult.lastResult!!.status == OK) {
+ notificationManager.cancelStatusNotification(jobResult)
} else {
- notificationManager.postStatusNotification(result)
+ notificationManager.postStatusNotification(jobResult)
}
checkStatusManager.scheduleCheck(
- site = result,
+ site = jobResult,
fromFinishingJob = true
)
}
@@ -148,28 +160,30 @@ class CheckStatusJob : JobService() {
}
private suspend fun updateStatus(
- site: ServerModel,
- status: ServerStatus = site.status
- ): ServerModel {
+ site: Site,
+ status: Status = site.lastResult?.status ?: WAITING
+ ): Site {
log("Updating ${site.name} (${site.url}) status to $status...")
val lastCheckTime =
- if (status.isPending()) site.lastCheck
+ if (status.isPending()) site.lastResult?.timestampMs ?: -1
else currentTimeMillis()
val reason =
if (status == OK) null
- else site.reason
+ else site.lastResult?.reason ?: "Unknown"
- val newSiteModel = site.copy(
+ val updatedModel = site.withStatus(
status = status,
- lastCheck = lastCheckTime,
+ timestamp = lastCheckTime,
reason = reason
)
- modelStore.update(newSiteModel)
+ database.updateSite(updatedModel)
withContext(Main) {
- sendBroadcast(Intent(ACTION_STATUS_UPDATE).apply { putExtra(KEY_UPDATE_MODEL, newSiteModel) })
+ sendBroadcast(Intent(ACTION_STATUS_UPDATE).apply {
+ putExtra(KEY_UPDATE_MODEL, updatedModel)
+ })
}
- return newSiteModel
+ return updatedModel
}
}
diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusManager.kt b/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/ValidationManager.kt
similarity index 71%
rename from engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusManager.kt
rename to engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/ValidationManager.kt
index cc50105..8fcb288 100644
--- a/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusManager.kt
+++ b/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/ValidationManager.kt
@@ -17,12 +17,13 @@ package com.afollestad.nocknock.engine.statuscheck
import android.app.job.JobScheduler
import android.app.job.JobScheduler.RESULT_SUCCESS
-import com.afollestad.nocknock.data.ServerModel
-import com.afollestad.nocknock.data.ServerStatus.ERROR
-import com.afollestad.nocknock.data.ServerStatus.OK
+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.db.ServerModelStore
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_SITE_ID
+import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.KEY_SITE_ID
import com.afollestad.nocknock.utilities.providers.BundleProvider
import com.afollestad.nocknock.utilities.providers.JobInfoProvider
import com.afollestad.nocknock.utilities.providers.StringProvider
@@ -37,37 +38,37 @@ import timber.log.Timber.d as log
/** @author Aidan Follestad (@afollestad) */
data class CheckResult(
- val model: ServerModel,
+ val model: Site,
val response: Response? = null
)
typealias ClientTimeoutChanger = (client: OkHttpClient, timeout: Int) -> OkHttpClient
/** @author Aidan Follestad (@afollestad) */
-interface CheckStatusManager {
+interface ValidationManager {
suspend fun ensureScheduledChecks()
fun scheduleCheck(
- site: ServerModel,
+ site: Site,
rightNow: Boolean = false,
cancelPrevious: Boolean = rightNow,
fromFinishingJob: Boolean = false
)
- fun cancelCheck(site: ServerModel)
+ fun cancelCheck(site: Site)
- suspend fun performCheck(site: ServerModel): CheckResult
+ suspend fun performCheck(site: Site): CheckResult
}
-class RealCheckStatusManager @Inject constructor(
+class RealValidationManager @Inject constructor(
private val jobScheduler: JobScheduler,
private val okHttpClient: OkHttpClient,
private val stringProvider: StringProvider,
private val bundleProvider: BundleProvider,
private val jobInfoProvider: JobInfoProvider,
- private val siteStore: ServerModelStore
-) : CheckStatusManager {
+ private val database: AppDatabase
+) : ValidationManager {
private var clientTimeoutChanger: ClientTimeoutChanger = { client, timeout ->
client.newBuilder()
@@ -76,12 +77,12 @@ class RealCheckStatusManager @Inject constructor(
}
override suspend fun ensureScheduledChecks() {
- val sites = siteStore.get()
+ val sites = database.allSites()
if (sites.isEmpty()) {
return
}
log("Ensuring enabled sites have scheduled checks.")
- sites.filter { !it.disabled }
+ sites.filter { it.settings?.disabled != true }
.forEach { site ->
val existingJob = jobForSite(site)
if (existingJob == null) {
@@ -94,12 +95,15 @@ class RealCheckStatusManager @Inject constructor(
}
override fun scheduleCheck(
- site: ServerModel,
+ site: Site,
rightNow: Boolean,
cancelPrevious: Boolean,
fromFinishingJob: Boolean
) {
- check(site.id != 0) { "Cannot schedule checks for jobs with no ID." }
+ check(site.id != 0L) { "Cannot schedule checks for jobs with no ID." }
+ val siteSettings = site.settings
+ requireNotNull(siteSettings) { "Site settings must be populated." }
+
if (cancelPrevious) {
cancelCheck(site)
} else if (!fromFinishingJob) {
@@ -111,18 +115,18 @@ class RealCheckStatusManager @Inject constructor(
log("Requesting a check job for site to be scheduled: $site")
val extras = bundleProvider.createPersistable {
- putInt(KEY_SITE_ID, site.id)
+ putLong(KEY_SITE_ID, site.id)
}
val jobInfo = jobInfoProvider.createCheckJob(
- id = site.id,
+ id = site.id.toInt(),
onlyUnmeteredNetwork = false,
delayMs = if (rightNow) {
1
} else {
- site.checkInterval
+ siteSettings.validationIntervalMs
},
extras = extras,
- target = CheckStatusJob::class.java
+ target = ValidationJob::class.java
)
val dispatchResult = jobScheduler.schedule(jobInfo)
@@ -133,15 +137,17 @@ class RealCheckStatusManager @Inject constructor(
}
}
- override fun cancelCheck(site: ServerModel) {
- check(site.id != 0) { "Cannot cancel scheduled checks for jobs with no ID." }
+ override fun cancelCheck(site: Site) {
+ check(site.id != 0L) { "Cannot cancel scheduled checks for jobs with no ID." }
log("Cancelling scheduled checks for site: ${site.id}")
- jobScheduler.cancel(site.id)
+ jobScheduler.cancel(site.id.toInt())
}
- override suspend fun performCheck(site: ServerModel): CheckResult {
- check(site.id != 0) { "Cannot schedule checks for jobs with no ID." }
- check(site.networkTimeout > 0) { "Network timeout not set for site ${site.id}" }
+ override suspend fun performCheck(site: Site): CheckResult {
+ check(site.id != 0L) { "Cannot schedule checks for jobs with no ID." }
+ val siteSettings = site.settings
+ requireNotNull(siteSettings) { "Site settings must be populated." }
+ check(siteSettings.networkTimeout > 0) { "Network timeout not set for site ${site.id}" }
log("performCheck(${site.id}) - GET ${site.url}")
val request = Request.Builder()
@@ -150,20 +156,20 @@ class RealCheckStatusManager @Inject constructor(
.build()
return try {
- val client = clientTimeoutChanger(okHttpClient, site.networkTimeout)
+ val client = clientTimeoutChanger(okHttpClient, siteSettings.networkTimeout)
val response = client.newCall(request)
.execute()
if (response.isSuccessful || response.code() == 401) {
log("performCheck(${site.id}) = Successful")
CheckResult(
- model = site.copy(status = OK, reason = null),
+ model = site.withStatus(status = OK, reason = null),
response = response
)
} else {
log("performCheck(${site.id}) = Failure, HTTP code ${response.code()}")
CheckResult(
- model = site.copy(
+ model = site.withStatus(
status = ERROR,
reason = "Response ${response.code()} - ${response.body()?.string() ?: "Unknown"}"
),
@@ -173,20 +179,20 @@ class RealCheckStatusManager @Inject constructor(
} catch (timeoutEx: SocketTimeoutException) {
log("performCheck(${site.id}) = Socket Timeout")
CheckResult(
- model = site.copy(
+ model = site.withStatus(
status = ERROR,
reason = stringProvider.get(R.string.timeout)
)
)
} catch (ex: Exception) {
log("performCheck(${site.id}) = Error: ${ex.message}")
- CheckResult(model = site.copy(status = ERROR, reason = ex.message))
+ CheckResult(model = site.withStatus(status = ERROR, reason = ex.message))
}
}
- private fun jobForSite(site: ServerModel) =
+ private fun jobForSite(site: Site) =
jobScheduler.allPendingJobs
- .firstOrNull { job -> job.id == site.id }
+ .firstOrNull { job -> job.id == site.id.toInt() }
@TestOnly fun setClientTimeoutChanger(changer: ClientTimeoutChanger) {
this.clientTimeoutChanger = changer
diff --git a/engine/src/test/java/com/afollestad/nocknock/engine/CheckStatusManagerTest.kt b/engine/src/test/java/com/afollestad/nocknock/engine/CheckStatusManagerTest.kt
index a97d034..071e407 100644
--- a/engine/src/test/java/com/afollestad/nocknock/engine/CheckStatusManagerTest.kt
+++ b/engine/src/test/java/com/afollestad/nocknock/engine/CheckStatusManagerTest.kt
@@ -17,13 +17,13 @@ package com.afollestad.nocknock.engine
import android.app.job.JobInfo
import android.app.job.JobScheduler
-import com.afollestad.nocknock.data.ServerModel
-import com.afollestad.nocknock.data.ServerStatus.ERROR
-import com.afollestad.nocknock.data.ServerStatus.OK
-import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
-import com.afollestad.nocknock.engine.db.ServerModelStore
-import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_SITE_ID
-import com.afollestad.nocknock.engine.statuscheck.RealCheckStatusManager
+import com.afollestad.nocknock.data.legacy.ServerModel
+import com.afollestad.nocknock.data.model.Status.ERROR
+import com.afollestad.nocknock.data.model.Status.OK
+import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
+import com.afollestad.nocknock.data.legacy.ServerModelStore
+import com.afollestad.nocknock.engine.statuscheck.ValidationJob.Companion.KEY_SITE_ID
+import com.afollestad.nocknock.engine.statuscheck.RealValidationManager
import com.afollestad.nocknock.utilities.providers.StringProvider
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.any
@@ -58,7 +58,7 @@ class CheckStatusManagerTest {
private val jobInfoProvider = testJobInfoProvider()
private val store = mock()
- private val manager = RealCheckStatusManager(
+ private val manager = RealValidationManager(
jobScheduler,
okHttpClient,
stringProvider,
diff --git a/notifications/build.gradle b/notifications/build.gradle
index 9578137..e843285 100644
--- a/notifications/build.gradle
+++ b/notifications/build.gradle
@@ -14,7 +14,6 @@ android {
}
dependencies {
- implementation project(':data')
implementation project(':utilities')
api 'androidx.appcompat:appcompat:' + versions.androidx
diff --git a/notifications/src/main/java/com/afollestad/nocknock/notifications/NockNotificationManager.kt b/notifications/src/main/java/com/afollestad/nocknock/notifications/NockNotificationManager.kt
index 73300dc..35acd46 100644
--- a/notifications/src/main/java/com/afollestad/nocknock/notifications/NockNotificationManager.kt
+++ b/notifications/src/main/java/com/afollestad/nocknock/notifications/NockNotificationManager.kt
@@ -18,9 +18,9 @@ package com.afollestad.nocknock.notifications
import android.annotation.TargetApi
import android.app.NotificationManager
import android.os.Build.VERSION_CODES
-import com.afollestad.nocknock.data.ServerModel
import com.afollestad.nocknock.notifications.Channel.CheckFailures
import com.afollestad.nocknock.utilities.providers.BitmapProvider
+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
@@ -37,9 +37,9 @@ interface NockNotificationManager {
fun createChannels()
- fun postStatusNotification(model: ServerModel)
+ fun postStatusNotification(model: CanNotifyModel)
- fun cancelStatusNotification(model: ServerModel)
+ fun cancelStatusNotification(model: CanNotifyModel)
fun cancelStatusNotifications()
}
@@ -65,32 +65,32 @@ class RealNockNotificationManager @Inject constructor(
override fun createChannels() =
Channel.values().forEach(this::createChannel)
- override fun postStatusNotification(model: ServerModel) {
+ override fun postStatusNotification(model: CanNotifyModel) {
if (isAppOpen) {
// Don't show notifications while the app is open
- log("App is open, status notification for site ${model.id} won't be posted.")
+ log("App is open, status notification for site ${model.notiId()} won't be posted.")
return
}
- log("Posting status notification for site ${model.id}...")
+ log("Posting status notification for site ${model.notiId()}...")
val intent = intentProvider.getPendingIntentForViewSite(model)
val newNotification = notificationProvider.create(
channelId = CheckFailures.id,
- title = model.name,
+ title = model.notiName(),
content = stringProvider.get(R.string.something_wrong),
intent = intent,
smallIcon = R.drawable.ic_notification,
largeIcon = bitmapProvider.get(appIconRes)
)
- stockManager.notify(model.url, model.notificationId(), newNotification)
+ stockManager.notify(model.notiTag(), model.notificationId(), newNotification)
log("Posted status notification for site ${model.notificationId()}.")
}
- override fun cancelStatusNotification(model: ServerModel) {
+ override fun cancelStatusNotification(model: CanNotifyModel) {
stockManager.cancel(model.notificationId())
- log("Cancelled status notification for site ${model.id}.")
+ log("Cancelled status notification for site ${model.notiId()}.")
}
override fun cancelStatusNotifications() = stockManager.cancelAll()
@@ -107,5 +107,5 @@ class RealNockNotificationManager @Inject constructor(
log("Created notification channel ${channel.id}")
}
- private fun ServerModel.notificationId() = BASE_NOTIFICATION_REQUEST_CODE + this.id
+ private fun CanNotifyModel.notificationId() = BASE_NOTIFICATION_REQUEST_CODE + this.notiId()
}
diff --git a/notifications/src/main/java/com/afollestad/nocknock/notifications/NotificationsModule.kt b/notifications/src/main/java/com/afollestad/nocknock/notifications/NotificationsModule.java
similarity index 70%
rename from notifications/src/main/java/com/afollestad/nocknock/notifications/NotificationsModule.kt
rename to notifications/src/main/java/com/afollestad/nocknock/notifications/NotificationsModule.java
index a5212bd..d43d339 100644
--- a/notifications/src/main/java/com/afollestad/nocknock/notifications/NotificationsModule.kt
+++ b/notifications/src/main/java/com/afollestad/nocknock/notifications/NotificationsModule.java
@@ -1,4 +1,4 @@
-/**
+/*
* Designed and developed by Aidan Follestad (@afollestad)
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,19 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.afollestad.nocknock.notifications
+package com.afollestad.nocknock.notifications;
-import dagger.Binds
-import dagger.Module
-import javax.inject.Singleton
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Singleton;
/** @author Aidan Follestad (@afollestad) */
@Module
-abstract class NotificationsModule {
+public abstract class NotificationsModule {
@Binds
@Singleton
- abstract fun provideNockNotificationManager(
- notificationManager: RealNockNotificationManager
- ): NockNotificationManager
+ abstract NockNotificationManager provideNockNotificationManager(
+ RealNockNotificationManager notificationManager);
}
diff --git a/notifications/src/test/java/com/afollestad/nocknock/notifications/NockNotificationManagerTest.kt b/notifications/src/test/java/com/afollestad/nocknock/notifications/NockNotificationManagerTest.kt
index c050eea..de614fa 100644
--- a/notifications/src/test/java/com/afollestad/nocknock/notifications/NockNotificationManagerTest.kt
+++ b/notifications/src/test/java/com/afollestad/nocknock/notifications/NockNotificationManagerTest.kt
@@ -20,8 +20,8 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.graphics.Bitmap
-import com.afollestad.nocknock.data.ServerModel
-import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
+import com.afollestad.nocknock.data.legacy.ServerModel
+import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
import com.afollestad.nocknock.notifications.Channel.CheckFailures
import com.afollestad.nocknock.utilities.providers.BitmapProvider
import com.afollestad.nocknock.utilities.providers.IntentProvider
diff --git a/utilities/src/main/java/com/afollestad/nocknock/utilities/UtilitiesModule.kt b/utilities/src/main/java/com/afollestad/nocknock/utilities/UtilitiesModule.java
similarity index 59%
rename from utilities/src/main/java/com/afollestad/nocknock/utilities/UtilitiesModule.kt
rename to utilities/src/main/java/com/afollestad/nocknock/utilities/UtilitiesModule.java
index a2a9770..5861004 100644
--- a/utilities/src/main/java/com/afollestad/nocknock/utilities/UtilitiesModule.kt
+++ b/utilities/src/main/java/com/afollestad/nocknock/utilities/UtilitiesModule.java
@@ -1,11 +1,11 @@
-/**
+/*
* 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
+ * 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,
@@ -13,77 +13,63 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.afollestad.nocknock.utilities
+package com.afollestad.nocknock.utilities;
-import com.afollestad.nocknock.utilities.providers.BitmapProvider
-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.RealBitmapProvider
-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.RealStringProvider
-import com.afollestad.nocknock.utilities.providers.SdkProvider
-import com.afollestad.nocknock.utilities.providers.StringProvider
-import dagger.Binds
-import dagger.Module
-import javax.inject.Singleton
+import com.afollestad.nocknock.utilities.providers.BitmapProvider;
+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.RealBitmapProvider;
+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.RealStringProvider;
+import com.afollestad.nocknock.utilities.providers.SdkProvider;
+import com.afollestad.nocknock.utilities.providers.StringProvider;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Singleton;
/** @author Aidan Follestad (@afollestad) */
@Module
-abstract class UtilitiesModule {
+public abstract class UtilitiesModule {
@Binds
@Singleton
- abstract fun provideSdkProvider(
- sdkProvider: RealSdkProvider
- ): SdkProvider
+ abstract SdkProvider provideSdkProvider(RealSdkProvider sdkProvider);
@Binds
@Singleton
- abstract fun provideBitmapProvider(
- bitmapProvider: RealBitmapProvider
- ): BitmapProvider
+ abstract BitmapProvider provideBitmapProvider(RealBitmapProvider bitmapProvider);
@Binds
@Singleton
- abstract fun provideStringProvider(
- stringProvider: RealStringProvider
- ): StringProvider
+ abstract StringProvider provideStringProvider(RealStringProvider stringProvider);
@Binds
@Singleton
- abstract fun provideIntentProvider(
- intentProvider: RealIntentProvider
- ): IntentProvider
+ abstract IntentProvider provideIntentProvider(RealIntentProvider intentProvider);
@Binds
@Singleton
- abstract fun provideChannelProvider(
- channelProvider: RealNotificationChannelProvider
- ): NotificationChannelProvider
+ abstract NotificationChannelProvider provideChannelProvider(
+ RealNotificationChannelProvider channelProvider);
@Binds
@Singleton
- abstract fun provideNotificationProvider(
- notificationProvider: RealNotificationProvider
- ): NotificationProvider
+ abstract NotificationProvider provideNotificationProvider(
+ RealNotificationProvider notificationProvider);
@Binds
@Singleton
- abstract fun provideBundleProvider(
- bundleProvider: RealBundleProvider
- ): BundleProvider
+ abstract BundleProvider provideBundleProvider(RealBundleProvider bundleProvider);
@Binds
@Singleton
- abstract fun provideJobInfoProvider(
- jobInfoProvider: RealJobInfoProvider
- ): JobInfoProvider
+ abstract JobInfoProvider provideJobInfoProvider(RealJobInfoProvider jobInfoProvider);
}
diff --git a/utilities/src/main/java/com/afollestad/nocknock/utilities/ext/ContentValuesExt.kt b/utilities/src/main/java/com/afollestad/nocknock/utilities/ext/ContentValuesExt.kt
deleted file mode 100644
index b13dfd0..0000000
--- a/utilities/src/main/java/com/afollestad/nocknock/utilities/ext/ContentValuesExt.kt
+++ /dev/null
@@ -1,59 +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.ContentValues
-
-/**
- * Returns a [ContentValues] instance which contains only values that have changed between
- * the receiver (original) and parameter (new) instances.
- */
-fun ContentValues.diffFrom(contentValues: ContentValues): ContentValues {
- val diff = ContentValues()
- for ((name, oldValue) in this.valueSet()) {
- val newValue = contentValues.get(name)
- if (newValue != oldValue) {
- diff.putAny(name, newValue)
- }
- }
- return diff
-}
-
-/**
- * Auto casts an [Any] value and uses the appropriate `put` method to store it
- * in the [ContentValues] instance.
- */
-fun ContentValues.putAny(
- name: String,
- value: Any?
-) {
- if (value == null) {
- putNull(name)
- return
- }
- when (value) {
- is String -> put(name, value)
- is Byte -> put(name, value)
- is Short -> put(name, value)
- is Int -> put(name, value)
- is Long -> put(name, value)
- is Float -> put(name, value)
- is Double -> put(name, value)
- is Boolean -> put(name, value)
- is ByteArray -> put(name, value)
- else -> throw IllegalArgumentException("ContentValues can't hold $value")
- }
-}
diff --git a/utilities/src/main/java/com/afollestad/nocknock/utilities/providers/BundleProvider.kt b/utilities/src/main/java/com/afollestad/nocknock/utilities/providers/BundleProvider.kt
index 8fc655b..3d60e81 100644
--- a/utilities/src/main/java/com/afollestad/nocknock/utilities/providers/BundleProvider.kt
+++ b/utilities/src/main/java/com/afollestad/nocknock/utilities/providers/BundleProvider.kt
@@ -19,9 +19,9 @@ import android.os.PersistableBundle
import javax.inject.Inject
interface IBundle {
- fun putInt(
+ fun putLong(
key: String,
- value: Int
+ value: Long
)
}
@@ -30,7 +30,7 @@ typealias IBundler = IBundle.() -> Unit
/** @author Aidan Follestad (@afollestad) */
interface BundleProvider {
- fun createPersistable(builder: IBundle.() -> Unit): PersistableBundle
+ fun createPersistable(bundler: IBundle.() -> Unit): PersistableBundle
}
/** @author Aidan Follestad (@afollestad) */
@@ -39,10 +39,10 @@ class RealBundleProvider @Inject constructor() : BundleProvider {
override fun createPersistable(bundler: IBundler): PersistableBundle {
val realBundle = PersistableBundle()
bundler(object : IBundle {
- override fun putInt(
+ override fun putLong(
key: String,
- value: Int
- ) = realBundle.putInt(key, value)
+ value: Long
+ ) = realBundle.putLong(key, value)
})
return realBundle
}
diff --git a/utilities/src/main/java/com/afollestad/nocknock/utilities/providers/IntentProvider.kt b/utilities/src/main/java/com/afollestad/nocknock/utilities/providers/IntentProvider.kt
index cee9298..56f65a0 100644
--- a/utilities/src/main/java/com/afollestad/nocknock/utilities/providers/IntentProvider.kt
+++ b/utilities/src/main/java/com/afollestad/nocknock/utilities/providers/IntentProvider.kt
@@ -24,16 +24,20 @@ import java.io.Serializable
import javax.inject.Inject
/** @author Aidan Follestad (@afollestad) */
-interface IdProvider : Serializable {
+interface CanNotifyModel : Serializable {
- fun id(): Int
+ fun notiId(): Int
+
+ fun notiName(): String
+
+ fun notiTag(): String
}
/** @author Aidan Follestad (@afollestad) */
interface IntentProvider {
fun getPendingIntentForViewSite(
- model: IdProvider
+ model: CanNotifyModel
): PendingIntent
}
@@ -48,17 +52,17 @@ class RealIntentProvider @Inject constructor(
const val KEY_VIEW_NOTIFICATION_MODEL = "model"
}
- override fun getPendingIntentForViewSite(model: IdProvider): PendingIntent {
+ override fun getPendingIntentForViewSite(model: CanNotifyModel): PendingIntent {
val openIntent = getIntentForViewSite(model)
return PendingIntent.getActivity(
app,
- BASE_NOTIFICATION_REQUEST_CODE + model.id(),
+ BASE_NOTIFICATION_REQUEST_CODE + model.notiId(),
openIntent,
FLAG_CANCEL_CURRENT
)
}
- private fun getIntentForViewSite(model: IdProvider) =
+ private fun getIntentForViewSite(model: CanNotifyModel) =
Intent(app, mainActivity).apply {
putExtra(KEY_VIEW_NOTIFICATION_MODEL, model)
}
diff --git a/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/LoadingIndicatorFrame.kt b/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/LoadingIndicatorFrame.kt
index 1e45e83..5596d4c 100644
--- a/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/LoadingIndicatorFrame.kt
+++ b/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/LoadingIndicatorFrame.kt
@@ -16,6 +16,7 @@
package com.afollestad.nocknock.viewcomponents
import android.content.Context
+import android.os.Handler
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.core.content.ContextCompat
@@ -32,6 +33,7 @@ class LoadingIndicatorFrame(
}
private val showRunnable = Runnable { show() }
+ private val delayHandler = Handler()
init {
setBackgroundColor(ContextCompat.getColor(context, R.color.loading_indicator_frame_background))
@@ -42,11 +44,11 @@ class LoadingIndicatorFrame(
}
fun setLoading() {
- handler.postDelayed(showRunnable, SHOW_DELAY_MS)
+ delayHandler.postDelayed(showRunnable, SHOW_DELAY_MS)
}
fun setDone() {
- handler.removeCallbacks(showRunnable)
+ delayHandler.removeCallbacks(showRunnable)
hide()
}
}
diff --git a/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/StatusImageView.kt b/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/StatusImageView.kt
index 9f907e6..96eb3cb 100644
--- a/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/StatusImageView.kt
+++ b/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/StatusImageView.kt
@@ -18,11 +18,11 @@ package com.afollestad.nocknock.viewcomponents
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
-import com.afollestad.nocknock.data.ServerStatus
-import com.afollestad.nocknock.data.ServerStatus.CHECKING
-import com.afollestad.nocknock.data.ServerStatus.ERROR
-import com.afollestad.nocknock.data.ServerStatus.OK
-import com.afollestad.nocknock.data.ServerStatus.WAITING
+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
/** @author Aidan Follestad (@afollestad) */
class StatusImageView(
@@ -34,7 +34,7 @@ class StatusImageView(
setStatus(OK)
}
- fun setStatus(status: ServerStatus) = when (status) {
+ fun setStatus(status: Status) = when (status) {
CHECKING, WAITING -> {
setImageResource(R.drawable.status_progress)
setBackgroundResource(R.drawable.yellow_circle)