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