diff --git a/app/src/main/java/com/afollestad/nocknock/adapter/ServerAdapter.kt b/app/src/main/java/com/afollestad/nocknock/adapter/ServerAdapter.kt
index 9792b86..c122113 100644
--- a/app/src/main/java/com/afollestad/nocknock/adapter/ServerAdapter.kt
+++ b/app/src/main/java/com/afollestad/nocknock/adapter/ServerAdapter.kt
@@ -46,7 +46,14 @@ class ServerVH constructor(
itemView.textStatus.setText(statusText)
}
- itemView.textInterval.text = model.intervalText()
+ if (model.disabled) {
+ itemView.textInterval.setText(R.string.checks_disabled)
+ } else {
+ itemView.textInterval.text = itemView.resources.getString(
+ R.string.next_check_x,
+ model.intervalText()
+ )
+ }
}
override fun onLongClick(view: View): Boolean {
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/AddSiteActivity.kt b/app/src/main/java/com/afollestad/nocknock/ui/AddSiteActivity.kt
index 6d4d861..43dd24b 100644
--- a/app/src/main/java/com/afollestad/nocknock/ui/AddSiteActivity.kt
+++ b/app/src/main/java/com/afollestad/nocknock/ui/AddSiteActivity.kt
@@ -55,7 +55,6 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
-import java.lang.System.currentTimeMillis
import javax.inject.Inject
import kotlin.math.max
import kotlin.properties.Delegates.notNull
@@ -233,7 +232,6 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
newModel = newModel.copy(
checkInterval = selectedCheckInterval,
- lastCheck = currentTimeMillis() - selectedCheckInterval,
validationMode = selectedValidationMode,
validationContent = selectedValidationMode.validationContent()
)
@@ -242,8 +240,12 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
launch(coroutineContext) {
loadingProgress.setLoading()
val storedModel = async(IO) { serverModelStore.put(newModel) }.await()
- checkStatusManager.cancelCheck(storedModel)
- checkStatusManager.scheduleCheck(storedModel, rightNow = true)
+
+ checkStatusManager.scheduleCheck(
+ site = storedModel,
+ rightNow = true,
+ cancelPrevious = true
+ )
loadingProgress.setDone()
setResult(RESULT_OK)
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/MainActivity.kt b/app/src/main/java/com/afollestad/nocknock/ui/MainActivity.kt
index 3cceb52..7411496 100644
--- a/app/src/main/java/com/afollestad/nocknock/ui/MainActivity.kt
+++ b/app/src/main/java/com/afollestad/nocknock/ui/MainActivity.kt
@@ -131,6 +131,7 @@ class MainActivity : AppCompatActivity() {
}
safeRegisterReceiver(intentReceiver, filter)
+ notificationManager.cancelStatusNotifications()
refreshModels()
}
@@ -171,10 +172,7 @@ class MainActivity : AppCompatActivity() {
title(R.string.options)
listItems(R.array.site_long_options) { _, i, _ ->
when (i) {
- 0 -> {
- checkStatusManager.cancelCheck(model)
- checkStatusManager.scheduleCheck(model)
- }
+ 0 -> checkStatusManager.scheduleCheck(site = model, cancelPrevious = true)
1 -> maybeRemoveSite(model) {
adapter.remove(i)
emptyText.showOrHide(adapter.itemCount == 0)
@@ -207,7 +205,7 @@ class MainActivity : AppCompatActivity() {
)
positiveButton(R.string.remove) {
checkStatusManager.cancelCheck(model)
- notificationManager.cancelStatusNotifications()
+ notificationManager.cancelStatusNotification(model)
performRemoveSite(model, onRemoved)
}
negativeButton(android.R.string.cancel)
diff --git a/app/src/main/java/com/afollestad/nocknock/ui/ViewSiteActivity.kt b/app/src/main/java/com/afollestad/nocknock/ui/ViewSiteActivity.kt
index cb6048d..eedd8f4 100644
--- a/app/src/main/java/com/afollestad/nocknock/ui/ViewSiteActivity.kt
+++ b/app/src/main/java/com/afollestad/nocknock/ui/ViewSiteActivity.kt
@@ -21,6 +21,7 @@ import androidx.annotation.CheckResult
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.text.HtmlCompat
+import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.nocknock.BuildConfig
import com.afollestad.nocknock.R
@@ -74,7 +75,6 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
-import java.lang.System.currentTimeMillis
import javax.inject.Inject
private const val KEY_VIEW_MODEL = "site_model"
@@ -228,7 +228,15 @@ class ViewSiteActivity : AppCompatActivity(),
}
}
+ disableChecksButton.setOnClickListener(this@ViewSiteActivity)
+ disableChecksButton.showOrHide(!this.disabled)
+
doneBtn.setOnClickListener(this@ViewSiteActivity)
+ doneBtn.setText(
+ if (this.disabled) R.string.renable_and_save_changes
+ else R.string.save_changes
+ )
+
invalidateMenuForStatus()
}
@@ -245,11 +253,139 @@ class ViewSiteActivity : AppCompatActivity(),
safeUnregisterReceiver(intentReceiver)
}
+ override fun onClick(view: View) = when (view.id) {
+ R.id.doneBtn -> performSaveChangesAndFinish()
+ R.id.disableChecksButton -> maybeDisableChecks()
+ else -> Unit
+ }
+
+ override fun onMenuItemClick(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.refresh -> performCheckNow()
+ R.id.remove -> maybeRemoveSite()
+ }
+ return true
+ }
+
+ private fun performCheckNow() {
+ rootView.scopeWhileAttached(Main) {
+ launch(coroutineContext) {
+ disableChecksButton.disable()
+ loadingProgress.setLoading()
+ updateModelFromInput(false)
+ currentModel = currentModel.copy(status = WAITING)
+ displayCurrentModel()
+
+ async(IO) { serverModelStore.update(currentModel) }.await()
+
+ checkStatusManager.scheduleCheck(
+ site = currentModel,
+ rightNow = true,
+ cancelPrevious = true
+ )
+ loadingProgress.setDone()
+ disableChecksButton.enable()
+ }
+ }
+ }
+
+ private fun maybeRemoveSite() {
+ MaterialDialog(this).show {
+ title(R.string.remove_site)
+ message(
+ text = HtmlCompat.fromHtml(
+ context.getString(R.string.remove_site_prompt, currentModel.name),
+ FROM_HTML_MODE_LEGACY
+ )
+ )
+ positiveButton(R.string.remove) {
+ checkStatusManager.cancelCheck(currentModel)
+ notificationManager.cancelStatusNotification(currentModel)
+ performRemoveSite()
+ }
+ negativeButton(android.R.string.cancel)
+ }
+ }
+
+ private fun performRemoveSite() {
+ rootView.scopeWhileAttached(Main) {
+ launch(coroutineContext) {
+ loadingProgress.setLoading()
+ async(IO) { serverModelStore.delete(currentModel) }.await()
+ loadingProgress.setDone()
+ finish()
+ }
+ }
+ }
+
+ private fun maybeDisableChecks() {
+ MaterialDialog(this).show {
+ title(R.string.disable_automatic_checks)
+ message(
+ text = HtmlCompat.fromHtml(
+ context.getString(R.string.disable_automatic_checks_prompt, currentModel.name),
+ FROM_HTML_MODE_LEGACY
+ )
+ )
+ positiveButton(R.string.disable) {
+ checkStatusManager.cancelCheck(currentModel)
+ notificationManager.cancelStatusNotification(currentModel)
+ performDisableChecks()
+ }
+ negativeButton(android.R.string.cancel)
+ }
+ }
+
+ private fun performDisableChecks() {
+ rootView.scopeWhileAttached(Main) {
+ launch(coroutineContext) {
+ loadingProgress.setLoading()
+ currentModel = currentModel.copy(
+ disabled = true,
+ lastCheck = LAST_CHECK_NONE
+ )
+ async(IO) { serverModelStore.update(currentModel) }.await()
+ loadingProgress.setDone()
+ displayCurrentModel() // invalidate UI to reflect disabled state
+ }
+ }
+ }
+
+ private fun performSaveChangesAndFinish() {
+ rootView.scopeWhileAttached(Main) {
+ launch(coroutineContext) {
+ loadingProgress.setLoading()
+ if (!updateModelFromInput(true)) {
+ // Validation didn't pass
+ loadingProgress.setDone()
+ return@launch
+ }
+
+ async(IO) { serverModelStore.update(currentModel) }.await()
+ checkStatusManager.scheduleCheck(
+ site = currentModel,
+ rightNow = true,
+ cancelPrevious = true
+ )
+
+ loadingProgress.setDone()
+ setResult(RESULT_OK)
+ finish()
+ }
+ }
+ }
+
+ private fun invalidateMenuForStatus() {
+ val item = toolbar.menu.findItem(R.id.refresh)
+ item.isEnabled = currentModel.status != CHECKING && currentModel.status != WAITING
+ }
+
@CheckResult private fun updateModelFromInput(withValidation: Boolean): Boolean {
currentModel = currentModel.copy(
name = inputName.trimmedText(),
url = inputUrl.trimmedText(),
- status = WAITING
+ status = WAITING,
+ disabled = false
)
if (withValidation && currentModel.name.isEmpty()) {
@@ -281,7 +417,6 @@ class ViewSiteActivity : AppCompatActivity(),
currentModel = currentModel.copy(
checkInterval = selectedCheckInterval,
- lastCheck = currentTimeMillis() - selectedCheckInterval,
validationMode = selectedValidationMode,
validationContent = selectedValidationMode.validationContent()
)
@@ -289,91 +424,6 @@ class ViewSiteActivity : AppCompatActivity(),
return true
}
- // Save button
- override fun onClick(view: View) {
- rootView.scopeWhileAttached(Main) {
- launch(coroutineContext) {
- loadingProgress.setLoading()
- if (!updateModelFromInput(true)) {
- // Validation didn't pass
- loadingProgress.setDone()
- return@launch
- }
-
- async(IO) { serverModelStore.update(currentModel) }.await()
- checkStatusManager.cancelCheck(currentModel)
- checkStatusManager.scheduleCheck(currentModel, rightNow = true)
-
- loadingProgress.setDone()
- setResult(RESULT_OK)
- finish()
- }
- }
- }
-
- override fun onMenuItemClick(item: MenuItem): Boolean {
- when (item.itemId) {
- R.id.refresh -> {
- rootView.scopeWhileAttached(Main) {
- launch(coroutineContext) {
- disableChecksButton.disable()
- loadingProgress.setLoading()
- updateModelFromInput(false)
- currentModel = currentModel.copy(status = WAITING)
- displayCurrentModel()
-
- async(IO) { serverModelStore.update(currentModel) }.await()
-
- checkStatusManager.cancelCheck(currentModel)
- checkStatusManager.scheduleCheck(currentModel, rightNow = true)
- loadingProgress.setDone()
- disableChecksButton.enable()
- }
- }
- return true
- }
- R.id.remove -> {
- maybeRemoveSite(currentModel)
- return true
- }
- }
- return false
- }
-
- private fun maybeRemoveSite(model: ServerModel) {
- MaterialDialog(this).show {
- title(R.string.remove_site)
- message(
- text = HtmlCompat.fromHtml(
- context.getString(R.string.remove_site_prompt, model.name),
- HtmlCompat.FROM_HTML_MODE_LEGACY
- )
- )
- positiveButton(R.string.remove) {
- checkStatusManager.cancelCheck(model)
- notificationManager.cancelStatusNotifications()
- performRemoveSite(model)
- }
- negativeButton(android.R.string.cancel)
- }
- }
-
- private fun performRemoveSite(model: ServerModel) {
- rootView.scopeWhileAttached(Main) {
- launch(coroutineContext) {
- loadingProgress.setLoading()
- async(IO) { serverModelStore.delete(model) }.await()
- loadingProgress.setDone()
- finish()
- }
- }
- }
-
- private fun invalidateMenuForStatus() {
- val item = toolbar.menu.findItem(R.id.refresh)
- item.isEnabled = currentModel.status != CHECKING && currentModel.status != WAITING
- }
-
private fun ValidationMode.validationContent() = when (this) {
STATUS_CODE -> null
TERM_SEARCH -> responseValidationSearchTerm.trimmedText()
diff --git a/app/src/main/res/layout/list_item_server.xml b/app/src/main/res/layout/list_item_server.xml
index e1e8728..e0fc469 100644
--- a/app/src/main/res/layout/list_item_server.xml
+++ b/app/src/main/res/layout/list_item_server.xml
@@ -57,7 +57,7 @@
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
- android:fontFamily="@font/lato_light"
+ android:fontFamily="@font/lato_black"
android:singleLine="true"
android:textColor="?android:textColorSecondary"
android:textSize="@dimen/caption_font_size"
@@ -71,7 +71,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/list_text_spacing"
- android:fontFamily="@font/lato"
+ android:fontFamily="@font/lato_light"
android:singleLine="true"
android:textColor="?android:textColorSecondary"
android:textSize="@dimen/body_font_size"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c0fa7eb..59dd474 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -30,13 +30,22 @@
%1$s from your sites?]]>
Remove
Save Changes
- Disable Automatic Checks
View Site
Last Check Result
Next Check
+ Next Check: %1$s
None (turned off)
None
+ Disable Automatic Checks
+ %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
+ Refresh icon at the top of this page.
+ ]]>
+ Disable
+ Enable Checks & Save Changes
+
Refresh Status
diff --git a/data/src/main/java/com/afollestad/nocknock/data/ServerModel.kt b/data/src/main/java/com/afollestad/nocknock/data/ServerModel.kt
index df10e4e..280a286 100644
--- a/data/src/main/java/com/afollestad/nocknock/data/ServerModel.kt
+++ b/data/src/main/java/com/afollestad/nocknock/data/ServerModel.kt
@@ -11,6 +11,7 @@ import com.afollestad.nocknock.data.ServerStatus.OK
import com.afollestad.nocknock.utilities.ext.timeString
import java.io.Serializable
import java.lang.System.currentTimeMillis
+import kotlin.math.max
const val CHECK_INTERVAL_UNSET = -1L
const val LAST_CHECK_NONE = -1L
@@ -62,12 +63,10 @@ data class ServerModel(
}
}
- fun intervalText() = if (checkInterval <= 0) {
- ""
- } else {
+ fun intervalText(): String {
val now = currentTimeMillis()
- val nextCheck = lastCheck + checkInterval
- (nextCheck - now).timeString()
+ val nextCheck = max(lastCheck, 0) + checkInterval
+ return (nextCheck - now).timeString()
}
fun toContentValues() = ContentValues().apply {
diff --git a/data/src/main/java/com/afollestad/nocknock/data/ServerStatus.kt b/data/src/main/java/com/afollestad/nocknock/data/ServerStatus.kt
index e1a5463..43a4e38 100644
--- a/data/src/main/java/com/afollestad/nocknock/data/ServerStatus.kt
+++ b/data/src/main/java/com/afollestad/nocknock/data/ServerStatus.kt
@@ -36,3 +36,5 @@ fun ServerStatus.textRes() = when (this) {
}
fun Int.toServerStatus() = ServerStatus.fromValue(this)
+
+fun ServerStatus.isPending() = this == WAITING || this == CHECKING
diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusJob.kt b/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusJob.kt
index 6f8a4c4..5abfacb 100644
--- a/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusJob.kt
+++ b/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusJob.kt
@@ -17,6 +17,7 @@ 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.engine.BuildConfig.APPLICATION_ID
import com.afollestad.nocknock.engine.db.ServerModelStore
import com.afollestad.nocknock.notifications.NockNotificationManager
@@ -127,7 +128,10 @@ class CheckStatusJob : JobService() {
notificationManager.postStatusNotification(result)
}
- checkStatusManager.scheduleCheck(result)
+ checkStatusManager.scheduleCheck(
+ site = result,
+ fromFinishingJob = true
+ )
}
return true
@@ -146,8 +150,8 @@ class CheckStatusJob : JobService() {
log("Updating ${site.name} (${site.url}) status to $status...")
val lastCheckTime =
- if (status == CHECKING) currentTimeMillis()
- else site.lastCheck
+ if (status.isPending()) site.lastCheck
+ else currentTimeMillis()
val reason =
if (status == OK) null
else site.reason
diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusManager.kt b/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusManager.kt
index 616718b..6c43d68 100644
--- a/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusManager.kt
+++ b/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusManager.kt
@@ -38,7 +38,9 @@ interface CheckStatusManager {
fun scheduleCheck(
site: ServerModel,
- rightNow: Boolean = false
+ rightNow: Boolean = false,
+ cancelPrevious: Boolean = false,
+ fromFinishingJob: Boolean = false
)
fun cancelCheck(site: ServerModel)
@@ -55,7 +57,6 @@ class RealCheckStatusManager @Inject constructor(
) : CheckStatusManager {
companion object {
-
private fun log(message: String) {
if (BuildConfig.DEBUG) {
Log.d("CheckStatusManager", message)
@@ -68,34 +69,43 @@ class RealCheckStatusManager @Inject constructor(
if (sites.isEmpty()) {
return
}
- log("Ensuring sites have scheduled checks.")
-
- sites.forEach { site ->
- val existingJob = jobScheduler.allPendingJobs
- .firstOrNull { job -> job.id == site.id }
- if (existingJob == null) {
- log("Site ${site.id} does NOT have a scheduled job, running one now.")
- scheduleCheck(site, rightNow = true)
- } else {
- log("Site ${site.id} already has a scheduled job. Nothing to do.")
- }
- }
+ log("Ensuring enabled sites have scheduled checks.")
+ sites.filter { !it.disabled }
+ .forEach { site ->
+ val existingJob = jobForSite(site)
+ if (existingJob == null) {
+ log("Site ${site.id} does NOT have a scheduled job, running one now.")
+ scheduleCheck(site = site, rightNow = true)
+ } else {
+ log("Site ${site.id} already has a scheduled job. Nothing to do.")
+ }
+ }
}
override fun scheduleCheck(
site: ServerModel,
- rightNow: Boolean
+ rightNow: Boolean,
+ cancelPrevious: Boolean,
+ fromFinishingJob: Boolean
) {
check(site.id != 0) { "Cannot schedule checks for jobs with no ID." }
- log("Requesting a check job for site to be scheduled: $site")
+ if (cancelPrevious) {
+ cancelCheck(site)
+ } else if (!fromFinishingJob) {
+ val existingJob = jobForSite(site)
+ check(existingJob == null) {
+ "Site ${site.id} already has a scheduled job, and cancelPrevious = false."
+ }
+ }
+ log("Requesting a check job for site to be scheduled: $site")
val extras = PersistableBundle().apply {
putInt(KEY_SITE_ID, site.id)
}
- // Note that we don't use the periodic feature of JobScheduler because it requires a
+ // Note: we don't use the periodic feature of JobScheduler because it requires a
// minimum of 15 minutes between each execution which may not be what's requested by the
- // user of this app.
+ // user of the app.
val jobInfo = jobInfo(app, site.id, CheckStatusJob::class.java) {
setRequiredNetworkType(NETWORK_TYPE_ANY)
if (rightNow) {
@@ -163,4 +173,8 @@ class RealCheckStatusManager @Inject constructor(
CheckResult(model = site.copy(status = ERROR, reason = ex.message))
}
}
+
+ private fun jobForSite(site: ServerModel) =
+ jobScheduler.allPendingJobs
+ .firstOrNull { job -> job.id == site.id }
}
diff --git a/utilities/src/main/java/com/afollestad/nocknock/utilities/ext/TimeExt.kt b/utilities/src/main/java/com/afollestad/nocknock/utilities/ext/TimeExt.kt
index 68dd5a3..b3fdc20 100644
--- a/utilities/src/main/java/com/afollestad/nocknock/utilities/ext/TimeExt.kt
+++ b/utilities/src/main/java/com/afollestad/nocknock/utilities/ext/TimeExt.kt
@@ -15,7 +15,7 @@ const val WEEK = DAY * 7
const val MONTH = WEEK * 4
fun Long.timeString() = when {
- this <= 0 -> ""
+ this <= 0 -> "??"
this >= MONTH ->
"${ceil((this.toFloat() / MONTH.toFloat()).toDouble()).toInt()}mo"
this >= WEEK ->
diff --git a/utilities/src/main/res/values/strings.xml b/utilities/src/main/res/values/strings.xml
index ba45d7d..6558f8a 100644
--- a/utilities/src/main/res/values/strings.xml
+++ b/utilities/src/main/res/values/strings.xml
@@ -1,3 +1,3 @@
- utilities
+ Checks Disabled