The ability to disable checks for sites, resolves #8.

This commit is contained in:
Aidan Follestad 2018-11-30 22:19:22 -08:00
parent ef73245831
commit 8193dd017d
12 changed files with 214 additions and 129 deletions

View file

@ -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 {

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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"

View file

@ -30,13 +30,22 @@
<string name="remove_site_prompt"><![CDATA[Remove <b>%1$s</b> from your sites?]]></string>
<string name="remove">Remove</string>
<string name="save_changes">Save Changes</string>
<string name="disable_automatic_checks">Disable Automatic Checks</string>
<string name="view_site">View Site</string>
<string name="last_check_result">Last Check Result</string>
<string name="next_check">Next Check</string>
<string name="next_check_x">Next Check: %1$s</string>
<string name="none_turned_off">None (turned off)</string>
<string name="none">None</string>
<string name="disable_automatic_checks">Disable Automatic Checks</string>
<string name="disable_automatic_checks_prompt"><![CDATA[
Disable automatic checks for <b>%1$s</b>? 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.
]]></string>
<string name="disable">Disable</string>
<string name="renable_and_save_changes">Enable Checks &amp; Save Changes</string>
<string name="refresh_status">Refresh Status</string>
<string name="warning_http_url">

View file

@ -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 {

View file

@ -36,3 +36,5 @@ fun ServerStatus.textRes() = when (this) {
}
fun Int.toServerStatus() = ServerStatus.fromValue(this)
fun ServerStatus.isPending() = this == WAITING || this == CHECKING

View file

@ -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

View file

@ -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 }
}

View file

@ -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 ->

View file

@ -1,3 +1,3 @@
<resources>
<string name="app_name">utilities</string>
<string name="checks_disabled">Checks Disabled</string>
</resources>