mirror of
https://github.com/afollestad/nock-nock.git
synced 2025-08-06 16:08:39 +00:00
Improved a lot of the UI, cleaned up some stuff. Add the ability to add headers to sites, resolves #39.
This commit is contained in:
parent
26ab76b363
commit
646bc25232
43 changed files with 846 additions and 278 deletions
|
@ -24,6 +24,7 @@ import androidx.room.Room.databaseBuilder
|
||||||
import com.afollestad.nocknock.data.AppDatabase
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.data.Database1to2Migration
|
import com.afollestad.nocknock.data.Database1to2Migration
|
||||||
import com.afollestad.nocknock.data.Database2to3Migration
|
import com.afollestad.nocknock.data.Database2to3Migration
|
||||||
|
import com.afollestad.nocknock.data.Database3to4Migration
|
||||||
import com.afollestad.nocknock.notifications.Qualifiers.MAIN_ACTIVITY_CLASS
|
import com.afollestad.nocknock.notifications.Qualifiers.MAIN_ACTIVITY_CLASS
|
||||||
import com.afollestad.nocknock.ui.main.MainActivity
|
import com.afollestad.nocknock.ui.main.MainActivity
|
||||||
import com.afollestad.nocknock.utilities.ext.systemService
|
import com.afollestad.nocknock.utilities.ext.systemService
|
||||||
|
@ -41,7 +42,8 @@ val mainModule = module {
|
||||||
databaseBuilder(get(), AppDatabase::class.java, "NockNock.db")
|
databaseBuilder(get(), AppDatabase::class.java, "NockNock.db")
|
||||||
.addMigrations(
|
.addMigrations(
|
||||||
Database1to2Migration(),
|
Database1to2Migration(),
|
||||||
Database2to3Migration()
|
Database2to3Migration(),
|
||||||
|
Database3to4Migration()
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,12 +21,14 @@ import android.widget.ArrayAdapter
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode
|
import com.afollestad.nocknock.data.model.ValidationMode
|
||||||
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
import com.afollestad.nocknock.ui.DarkModeSwitchActivity
|
||||||
|
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
|
||||||
|
import com.afollestad.nocknock.viewcomponents.ext.onScroll
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
|
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
|
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout
|
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.doneBtn
|
import kotlinx.android.synthetic.main.activity_addsite.headersLayout
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.inputName
|
import kotlinx.android.synthetic.main.activity_addsite.inputName
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.inputTags
|
import kotlinx.android.synthetic.main.activity_addsite.inputTags
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.inputUrl
|
import kotlinx.android.synthetic.main.activity_addsite.inputUrl
|
||||||
|
@ -36,10 +38,12 @@ import kotlinx.android.synthetic.main.activity_addsite.responseValidationMode
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchTerm
|
import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchTerm
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.retryPolicyLayout
|
import kotlinx.android.synthetic.main.activity_addsite.retryPolicyLayout
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout
|
import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout
|
||||||
|
import kotlinx.android.synthetic.main.activity_addsite.scrollView
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning
|
import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
|
import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
|
||||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
import kotlinx.android.synthetic.main.include_app_bar.app_toolbar as appToolbar
|
||||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
|
import kotlinx.android.synthetic.main.include_app_bar.toolbar_title as toolbarTitle
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
|
@ -118,11 +122,24 @@ class AddSiteActivity : DarkModeSwitchActivity() {
|
||||||
timesData = viewModel.retryPolicyTimes,
|
timesData = viewModel.retryPolicyTimes,
|
||||||
minutesData = viewModel.retryPolicyMinutes
|
minutesData = viewModel.retryPolicyMinutes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
headersLayout.attach(viewModel.headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUi() {
|
private fun setupUi() {
|
||||||
toolbarTitle.setText(R.string.add_site)
|
toolbarTitle.setText(R.string.add_site)
|
||||||
toolbar.run {
|
toolbar.run {
|
||||||
|
inflateMenu(R.menu.menu_addsite)
|
||||||
|
setOnMenuItemClickListener {
|
||||||
|
if (it.itemId == R.id.commit) {
|
||||||
|
viewModel.commit {
|
||||||
|
setResult(RESULT_OK)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
setNavigationIcon(R.drawable.ic_action_close)
|
setNavigationIcon(R.drawable.ic_action_close)
|
||||||
setNavigationOnClickListener { finish() }
|
setNavigationOnClickListener { finish() }
|
||||||
}
|
}
|
||||||
|
@ -135,11 +152,11 @@ class AddSiteActivity : DarkModeSwitchActivity() {
|
||||||
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
||||||
responseValidationMode.adapter = validationOptionsAdapter
|
responseValidationMode.adapter = validationOptionsAdapter
|
||||||
|
|
||||||
// Done button
|
scrollView.onScroll {
|
||||||
doneBtn.setOnClickListener {
|
appToolbar.elevation = if (it > appToolbar.measuredHeight / 2) {
|
||||||
viewModel.commit {
|
appToolbar.dimenFloat(R.dimen.default_elevation)
|
||||||
setResult(RESULT_OK)
|
} else {
|
||||||
finish()
|
0f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
import androidx.lifecycle.OnLifecycleEvent
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.AppDatabase
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
|
import com.afollestad.nocknock.data.model.Header
|
||||||
import com.afollestad.nocknock.data.model.RetryPolicy
|
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.SiteSettings
|
import com.afollestad.nocknock.data.model.SiteSettings
|
||||||
|
@ -35,7 +36,7 @@ import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||||
import com.afollestad.nocknock.data.model.ValidationResult
|
import com.afollestad.nocknock.data.model.ValidationResult
|
||||||
import com.afollestad.nocknock.data.putSite
|
import com.afollestad.nocknock.data.putSite
|
||||||
import com.afollestad.nocknock.engine.validation.ValidationManager
|
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||||
import com.afollestad.nocknock.ui.ScopedViewModel
|
import com.afollestad.nocknock.ui.ScopedViewModel
|
||||||
import com.afollestad.nocknock.utilities.ext.MINUTE
|
import com.afollestad.nocknock.utilities.ext.MINUTE
|
||||||
import com.afollestad.nocknock.utilities.livedata.map
|
import com.afollestad.nocknock.utilities.livedata.map
|
||||||
|
@ -49,7 +50,7 @@ import java.lang.System.currentTimeMillis
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class AddSiteViewModel(
|
class AddSiteViewModel(
|
||||||
private val database: AppDatabase,
|
private val database: AppDatabase,
|
||||||
private val validationManager: ValidationManager,
|
private val validationManager: ValidationExecutor,
|
||||||
mainDispatcher: CoroutineDispatcher,
|
mainDispatcher: CoroutineDispatcher,
|
||||||
private val ioDispatcher: CoroutineDispatcher
|
private val ioDispatcher: CoroutineDispatcher
|
||||||
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
|
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
|
||||||
|
@ -66,6 +67,7 @@ class AddSiteViewModel(
|
||||||
val checkIntervalUnit = MutableLiveData<Long>()
|
val checkIntervalUnit = MutableLiveData<Long>()
|
||||||
val retryPolicyTimes = MutableLiveData<Int>()
|
val retryPolicyTimes = MutableLiveData<Int>()
|
||||||
val retryPolicyMinutes = MutableLiveData<Int>()
|
val retryPolicyMinutes = MutableLiveData<Int>()
|
||||||
|
val headers = MutableLiveData<List<Header>>()
|
||||||
|
|
||||||
@OnLifecycleEvent(ON_START)
|
@OnLifecycleEvent(ON_START)
|
||||||
fun setDefaults() {
|
fun setDefaults() {
|
||||||
|
@ -76,6 +78,7 @@ class AddSiteViewModel(
|
||||||
retryPolicyMinutes.value = 0
|
retryPolicyMinutes.value = 0
|
||||||
retryPolicyMinutes.value = 0
|
retryPolicyMinutes.value = 0
|
||||||
tags.value = ""
|
tags.value = ""
|
||||||
|
headers.value = emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private properties
|
// Private properties
|
||||||
|
@ -134,7 +137,7 @@ class AddSiteViewModel(
|
||||||
val storedModel = withContext(ioDispatcher) {
|
val storedModel = withContext(ioDispatcher) {
|
||||||
database.putSite(newModel)
|
database.putSite(newModel)
|
||||||
}
|
}
|
||||||
validationManager.scheduleCheck(
|
validationManager.scheduleValidation(
|
||||||
site = storedModel,
|
site = storedModel,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true
|
cancelPrevious = true
|
||||||
|
@ -260,7 +263,8 @@ class AddSiteViewModel(
|
||||||
tags = cleanedTags,
|
tags = cleanedTags,
|
||||||
settings = newSettings,
|
settings = newSettings,
|
||||||
lastResult = newLastResult,
|
lastResult = newLastResult,
|
||||||
retryPolicy = newRetryPolicy
|
retryPolicy = newRetryPolicy,
|
||||||
|
headers = headers.value ?: emptyList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.data.allSites
|
import com.afollestad.nocknock.data.allSites
|
||||||
import com.afollestad.nocknock.data.deleteSite
|
import com.afollestad.nocknock.data.deleteSite
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.engine.validation.ValidationManager
|
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.ui.ScopedViewModel
|
import com.afollestad.nocknock.ui.ScopedViewModel
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
@ -36,7 +36,7 @@ import kotlinx.coroutines.withContext
|
||||||
class MainViewModel(
|
class MainViewModel(
|
||||||
private val database: AppDatabase,
|
private val database: AppDatabase,
|
||||||
private val notificationManager: NockNotificationManager,
|
private val notificationManager: NockNotificationManager,
|
||||||
private val validationManager: ValidationManager,
|
private val validationManager: ValidationExecutor,
|
||||||
mainDispatcher: CoroutineDispatcher,
|
mainDispatcher: CoroutineDispatcher,
|
||||||
private val ioDispatcher: CoroutineDispatcher
|
private val ioDispatcher: CoroutineDispatcher
|
||||||
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
|
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
|
||||||
|
@ -73,7 +73,7 @@ class MainViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshSite(model: Site) {
|
fun refreshSite(model: Site) {
|
||||||
validationManager.scheduleCheck(
|
validationManager.scheduleValidation(
|
||||||
site = model,
|
site = model,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true
|
cancelPrevious = true
|
||||||
|
@ -81,7 +81,7 @@ class MainViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeSite(model: Site) {
|
fun removeSite(model: Site) {
|
||||||
validationManager.cancelCheck(model)
|
validationManager.cancelScheduledValidation(model)
|
||||||
notificationManager.cancelStatusNotification(model)
|
notificationManager.cancelStatusNotification(model)
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@ -134,7 +134,7 @@ class MainViewModel(
|
||||||
|
|
||||||
private suspend fun ensureCheckJobs() {
|
private suspend fun ensureCheckJobs() {
|
||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
validationManager.ensureScheduledChecks()
|
validationManager.ensureScheduledValidations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,8 +33,7 @@ import com.afollestad.nocknock.viewcomponents.livedata.toViewError
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
|
import com.afollestad.nocknock.viewcomponents.livedata.toViewText
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalLayout
|
import kotlinx.android.synthetic.main.activity_viewsite.checkIntervalLayout
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.disableChecksButton
|
import kotlinx.android.synthetic.main.activity_viewsite.headersLayout
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.doneBtn
|
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.iconStatus
|
import kotlinx.android.synthetic.main.activity_viewsite.iconStatus
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.inputName
|
import kotlinx.android.synthetic.main.activity_viewsite.inputName
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.inputTags
|
import kotlinx.android.synthetic.main.activity_viewsite.inputTags
|
||||||
|
@ -50,6 +49,7 @@ import kotlinx.android.synthetic.main.activity_viewsite.textLastCheckResult
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck
|
import kotlinx.android.synthetic.main.activity_viewsite.textNextCheck
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
|
import kotlinx.android.synthetic.main.activity_viewsite.textUrlWarning
|
||||||
import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription
|
import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescription
|
||||||
|
import kotlinx.android.synthetic.main.include_app_bar.app_toolbar as appToolbar
|
||||||
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
import kotlinx.android.synthetic.main.include_app_bar.toolbar
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
@ -148,6 +148,9 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
|
||||||
minutesData = viewModel.retryPolicyMinutes
|
minutesData = viewModel.retryPolicyMinutes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
headersLayout.attach(viewModel.headers)
|
||||||
|
|
||||||
// Last/next check
|
// Last/next check
|
||||||
viewModel.onLastCheckResultText()
|
viewModel.onLastCheckResultText()
|
||||||
.toViewText(this, textLastCheckResult)
|
.toViewText(this, textLastCheckResult)
|
||||||
|
@ -156,25 +159,31 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUi() {
|
private fun setupUi() {
|
||||||
toolbarTitle.setText(R.string.view_site)
|
toolbarTitle.text = ""
|
||||||
toolbar.run {
|
toolbar.run {
|
||||||
setNavigationIcon(R.drawable.ic_action_close)
|
setNavigationIcon(R.drawable.ic_action_close)
|
||||||
setNavigationOnClickListener { finish() }
|
setNavigationOnClickListener { finish() }
|
||||||
inflateMenu(R.menu.menu_viewsite)
|
inflateMenu(R.menu.menu_viewsite)
|
||||||
|
|
||||||
menu.findItem(R.id.refresh)
|
menu.findItem(R.id.refresh)
|
||||||
.setActionView(R.layout.menu_item_refresh_icon)
|
.setActionView(R.layout.menu_item_refresh_icon)
|
||||||
.apply {
|
.apply {
|
||||||
actionView.setOnClickListener { viewModel.checkNow() }
|
actionView.setOnClickListener { viewModel.checkNow() }
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnMenuItemClickListener {
|
setOnMenuItemClickListener {
|
||||||
maybeRemoveSite()
|
when (it.itemId) {
|
||||||
|
R.id.commit -> viewModel.commit { finish() }
|
||||||
|
R.id.remove -> maybeRemoveSite()
|
||||||
|
R.id.disableChecks -> maybeDisableChecks()
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollView.onScroll {
|
scrollView.onScroll {
|
||||||
toolbar.elevation = if (it > toolbar.height / 4) {
|
appToolbar.elevation = if (it > appToolbar.measuredHeight / 2) {
|
||||||
toolbar.dimenFloat(R.dimen.default_elevation)
|
appToolbar.dimenFloat(R.dimen.default_elevation)
|
||||||
} else {
|
} else {
|
||||||
0f
|
0f
|
||||||
}
|
}
|
||||||
|
@ -190,15 +199,17 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
|
||||||
|
|
||||||
// Disabled button
|
// Disabled button
|
||||||
viewModel.onDisableChecksVisibility()
|
viewModel.onDisableChecksVisibility()
|
||||||
.toViewVisibility(this, disableChecksButton)
|
.observe(this, Observer {
|
||||||
disableChecksButton.setOnClickListener { maybeDisableChecks() }
|
toolbar.menu.findItem(R.id.disableChecks)
|
||||||
|
.isVisible = it
|
||||||
|
})
|
||||||
|
|
||||||
// Done button
|
// Done button
|
||||||
viewModel.onDoneButtonText()
|
viewModel.onDoneButtonText()
|
||||||
.toViewText(this, doneBtn)
|
.observe(this, Observer {
|
||||||
doneBtn.setOnClickListener {
|
toolbar.menu.findItem(R.id.commit)
|
||||||
viewModel.commit { finish() }
|
.setTitle(it)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.AppDatabase
|
import com.afollestad.nocknock.data.AppDatabase
|
||||||
import com.afollestad.nocknock.data.model.RetryPolicy
|
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||||
import com.afollestad.nocknock.data.deleteSite
|
import com.afollestad.nocknock.data.deleteSite
|
||||||
|
import com.afollestad.nocknock.data.model.Header
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.Status
|
import com.afollestad.nocknock.data.model.Status
|
||||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||||
|
@ -35,7 +36,7 @@ import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||||
import com.afollestad.nocknock.data.model.ValidationResult
|
import com.afollestad.nocknock.data.model.ValidationResult
|
||||||
import com.afollestad.nocknock.data.model.textRes
|
import com.afollestad.nocknock.data.model.textRes
|
||||||
import com.afollestad.nocknock.data.updateSite
|
import com.afollestad.nocknock.data.updateSite
|
||||||
import com.afollestad.nocknock.engine.validation.ValidationManager
|
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.ui.ScopedViewModel
|
import com.afollestad.nocknock.ui.ScopedViewModel
|
||||||
import com.afollestad.nocknock.utilities.ext.formatDate
|
import com.afollestad.nocknock.utilities.ext.formatDate
|
||||||
|
@ -54,7 +55,7 @@ class ViewSiteViewModel(
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val database: AppDatabase,
|
private val database: AppDatabase,
|
||||||
private val notificationManager: NockNotificationManager,
|
private val notificationManager: NockNotificationManager,
|
||||||
private val validationManager: ValidationManager,
|
private val validationManager: ValidationExecutor,
|
||||||
mainDispatcher: CoroutineDispatcher,
|
mainDispatcher: CoroutineDispatcher,
|
||||||
private val ioDispatcher: CoroutineDispatcher
|
private val ioDispatcher: CoroutineDispatcher
|
||||||
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
|
) : ScopedViewModel(mainDispatcher), LifecycleObserver {
|
||||||
|
@ -74,6 +75,7 @@ class ViewSiteViewModel(
|
||||||
val checkIntervalUnit = MutableLiveData<Long>()
|
val checkIntervalUnit = MutableLiveData<Long>()
|
||||||
val retryPolicyTimes = MutableLiveData<Int>()
|
val retryPolicyTimes = MutableLiveData<Int>()
|
||||||
val retryPolicyMinutes = MutableLiveData<Int>()
|
val retryPolicyMinutes = MutableLiveData<Int>()
|
||||||
|
val headers = MutableLiveData<List<Header>>()
|
||||||
internal val disabled = MutableLiveData<Boolean>()
|
internal val disabled = MutableLiveData<Boolean>()
|
||||||
internal val lastResult = MutableLiveData<ValidationResult?>()
|
internal val lastResult = MutableLiveData<ValidationResult?>()
|
||||||
|
|
||||||
|
@ -169,7 +171,7 @@ class ViewSiteViewModel(
|
||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
database.updateSite(updatedModel)
|
database.updateSite(updatedModel)
|
||||||
}
|
}
|
||||||
validationManager.scheduleCheck(
|
validationManager.scheduleValidation(
|
||||||
site = updatedModel,
|
site = updatedModel,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true
|
cancelPrevious = true
|
||||||
|
@ -185,7 +187,7 @@ class ViewSiteViewModel(
|
||||||
status = WAITING
|
status = WAITING
|
||||||
)
|
)
|
||||||
setModel(checkModel)
|
setModel(checkModel)
|
||||||
validationManager.scheduleCheck(
|
validationManager.scheduleValidation(
|
||||||
site = checkModel,
|
site = checkModel,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true
|
cancelPrevious = true
|
||||||
|
@ -193,7 +195,7 @@ class ViewSiteViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeSite(done: () -> Unit) {
|
fun removeSite(done: () -> Unit) {
|
||||||
validationManager.cancelCheck(site)
|
validationManager.cancelScheduledValidation(site)
|
||||||
notificationManager.cancelStatusNotification(site)
|
notificationManager.cancelStatusNotification(site)
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@ -207,7 +209,7 @@ class ViewSiteViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disableSite() {
|
fun disableSite() {
|
||||||
validationManager.cancelCheck(site)
|
validationManager.cancelScheduledValidation(site)
|
||||||
notificationManager.cancelStatusNotification(site)
|
notificationManager.cancelStatusNotification(site)
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
@ -339,7 +341,8 @@ class ViewSiteViewModel(
|
||||||
tags = cleanedTags,
|
tags = cleanedTags,
|
||||||
url = url.value!!.trim(),
|
url = url.value!!.trim(),
|
||||||
settings = newSettings,
|
settings = newSettings,
|
||||||
retryPolicy = retryPolicy
|
retryPolicy = retryPolicy,
|
||||||
|
headers = headers.value ?: emptyList()
|
||||||
)
|
)
|
||||||
.withStatus(status = WAITING)
|
.withStatus(status = WAITING)
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ fun ViewSiteViewModel.setModel(site: Site) {
|
||||||
|
|
||||||
setCheckInterval(settings.validationIntervalMs)
|
setCheckInterval(settings.validationIntervalMs)
|
||||||
setRetryPolicy(site.retryPolicy)
|
setRetryPolicy(site.retryPolicy)
|
||||||
|
headers.value = site.headers
|
||||||
|
|
||||||
this.disabled.value = settings.disabled
|
this.disabled.value = settings.disabled
|
||||||
this.lastResult.value = site.lastResult
|
this.lastResult.value = site.lastResult
|
||||||
|
|
10
app/src/main/res/drawable/ic_check.xml
Normal file
10
app/src/main/res/drawable/ic_check.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="24.0"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:width="24dp">
|
||||||
|
<path
|
||||||
|
android:fillColor="?iconColor"
|
||||||
|
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||||
|
</vector>
|
|
@ -16,6 +16,7 @@
|
||||||
<include layout="@layout/include_app_bar"/>
|
<include layout="@layout/include_app_bar"/>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
android:id="@+id/scrollView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
>
|
>
|
||||||
|
@ -24,89 +25,62 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingBottom="@dimen/content_inset"
|
android:paddingBottom="@dimen/content_inset_double"
|
||||||
android:paddingLeft="@dimen/content_inset"
|
android:paddingLeft="@dimen/content_inset"
|
||||||
android:paddingRight="@dimen/content_inset"
|
android:paddingRight="@dimen/content_inset"
|
||||||
android:paddingTop="@dimen/content_inset_half"
|
android:paddingTop="@dimen/content_inset_half"
|
||||||
>
|
>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<TextView
|
||||||
android:id="@+id/nameTiLayout"
|
android:layout_marginTop="0dp"
|
||||||
android:layout_width="match_parent"
|
android:text="@string/site_name"
|
||||||
android:layout_height="wrap_content"
|
style="@style/InputForm.Header"
|
||||||
android:layout_marginLeft="-4dp"
|
/>
|
||||||
android:layout_marginRight="-4dp"
|
|
||||||
android:nextFocusDown="@+id/urlTiLayout"
|
|
||||||
>
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<EditText
|
||||||
android:id="@+id/inputName"
|
android:id="@+id/inputName"
|
||||||
android:layout_width="match_parent"
|
android:hint="@string/site_name_hint"
|
||||||
android:layout_height="wrap_content"
|
android:inputType="textPersonName|textCapWords|textAutoCorrect"
|
||||||
android:hint="@string/site_name"
|
android:nextFocusDown="@+id/inputUrl"
|
||||||
android:inputType="textPersonName|textCapWords|textAutoCorrect"
|
tools:ignore="Autofill"
|
||||||
android:nextFocusDown="@+id/inputUrl"
|
style="@style/InputForm.Field"
|
||||||
style="@style/NockText.Body"
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
<TextView
|
||||||
|
android:text="@string/site_url"
|
||||||
|
style="@style/InputForm.Header"
|
||||||
|
/>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<EditText
|
||||||
android:id="@+id/urlTiLayout"
|
android:id="@+id/inputUrl"
|
||||||
android:layout_width="match_parent"
|
android:hint="@string/site_url_hint"
|
||||||
android:layout_height="wrap_content"
|
android:inputType="textUri"
|
||||||
android:layout_marginLeft="-4dp"
|
android:nextFocusDown="@+id/inputTags"
|
||||||
android:layout_marginRight="-4dp"
|
tools:ignore="Autofill"
|
||||||
android:layout_marginTop="@dimen/content_inset_half"
|
style="@style/InputForm.Field"
|
||||||
android:nextFocusDown="@+id/tagsTiLayout"
|
/>
|
||||||
>
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/inputUrl"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/site_url"
|
|
||||||
android:inputType="textUri"
|
|
||||||
android:nextFocusDown="@+id/inputTags"
|
|
||||||
style="@style/NockText.Body"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textUrlWarning"
|
android:id="@+id/textUrlWarning"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/list_text_spacing"
|
|
||||||
android:text="@string/warning_http_url"
|
android:text="@string/warning_http_url"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
style="@style/NockText.Footnote"
|
style="@style/InputForm.FieldNote"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<TextView
|
||||||
android:id="@+id/tagsTiLayout"
|
android:text="@string/site_tags"
|
||||||
android:layout_width="match_parent"
|
style="@style/InputForm.Header"
|
||||||
android:layout_height="wrap_content"
|
/>
|
||||||
android:layout_marginLeft="-4dp"
|
|
||||||
android:layout_marginRight="-4dp"
|
|
||||||
android:layout_marginTop="@dimen/content_inset_half"
|
|
||||||
android:nextFocusDown="@+id/urlTiLayout"
|
|
||||||
>
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<EditText
|
||||||
android:id="@+id/inputTags"
|
android:id="@+id/inputTags"
|
||||||
android:layout_width="match_parent"
|
android:digits=",.?-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ "
|
||||||
android:layout_height="wrap_content"
|
android:hint="@string/site_tags_hint"
|
||||||
android:digits=",.?-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ "
|
android:inputType="text|textCapWords"
|
||||||
android:hint="@string/site_tags_hint"
|
android:nextFocusDown="@+id/inputUrl"
|
||||||
android:imeOptions="actionNext"
|
tools:ignore="Autofill"
|
||||||
android:inputType="text|textCapWords"
|
style="@style/InputForm.Field"
|
||||||
android:nextFocusDown="@+id/inputUrl"
|
/>
|
||||||
android:singleLine="true"
|
|
||||||
style="@style/NockText.Body"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<include layout="@layout/include_divider"/>
|
<include layout="@layout/include_divider"/>
|
||||||
|
|
||||||
|
@ -118,34 +92,23 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/content_inset_less"
|
|
||||||
android:text="@string/response_timeout"
|
android:text="@string/response_timeout"
|
||||||
style="@style/NockText.SectionHeader"
|
style="@style/InputForm.Header"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/responseTimeoutInput"
|
android:id="@+id/responseTimeoutInput"
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="-4dp"
|
|
||||||
android:layout_marginStart="-4dp"
|
|
||||||
android:layout_marginTop="@dimen/content_inset_quarter"
|
|
||||||
android:hint="@string/response_timeout_default"
|
android:hint="@string/response_timeout_default"
|
||||||
android:inputType="number"
|
android:inputType="number"
|
||||||
android:maxLength="8"
|
android:maxLength="8"
|
||||||
tools:ignore="Autofill,HardcodedText,LabelFor"
|
tools:ignore="Autofill"
|
||||||
style="@style/NockText.Body"
|
style="@style/InputForm.Field"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/responseValidationLabel"
|
android:id="@+id/responseValidationLabel"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/content_inset_more"
|
|
||||||
android:text="@string/response_validation_mode"
|
android:text="@string/response_validation_mode"
|
||||||
style="@style/NockText.SectionHeader"
|
style="@style/InputForm.Header"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Spinner
|
<Spinner
|
||||||
|
@ -187,12 +150,7 @@
|
||||||
style="@style/NockText.Body.Light"
|
style="@style/NockText.Body.Light"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View
|
<include layout="@layout/include_divider"/>
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:layout_marginTop="@dimen/content_inset_less"
|
|
||||||
android:background="?dividerColor"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<com.afollestad.nocknock.viewcomponents.retrypolicy.RetryPolicyLayout
|
<com.afollestad.nocknock.viewcomponents.retrypolicy.RetryPolicyLayout
|
||||||
android:id="@+id/retryPolicyLayout"
|
android:id="@+id/retryPolicyLayout"
|
||||||
|
@ -201,13 +159,13 @@
|
||||||
android:layout_marginTop="@dimen/content_inset_more"
|
android:layout_marginTop="@dimen/content_inset_more"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<include layout="@layout/include_divider"/>
|
||||||
android:id="@+id/doneBtn"
|
|
||||||
|
<com.afollestad.nocknock.viewcomponents.headers.HeaderStackLayout
|
||||||
|
android:id="@+id/headersLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/content_inset_double"
|
android:layout_marginTop="@dimen/content_inset_half"
|
||||||
android:text="@string/add_site"
|
|
||||||
style="@style/AccentButton"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -29,9 +29,23 @@
|
||||||
android:paddingBottom="@dimen/content_inset"
|
android:paddingBottom="@dimen/content_inset"
|
||||||
android:paddingLeft="@dimen/content_inset"
|
android:paddingLeft="@dimen/content_inset"
|
||||||
android:paddingRight="@dimen/content_inset"
|
android:paddingRight="@dimen/content_inset"
|
||||||
android:paddingTop="@dimen/content_inset_half"
|
android:paddingTop="@dimen/content_inset_less"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/inputName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@null"
|
||||||
|
android:hint="@string/site_name"
|
||||||
|
android:inputType="textPersonName|textCapWords|textAutoCorrect"
|
||||||
|
android:nextFocusDown="@+id/inputUrl"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:transitionName="site_name"
|
||||||
|
tools:ignore="Autofill,UnusedAttribute"
|
||||||
|
style="@style/NockText.Header"
|
||||||
|
/>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -55,19 +69,6 @@
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
>
|
>
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/inputName"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/site_name"
|
|
||||||
android:inputType="textPersonName|textCapWords|textAutoCorrect"
|
|
||||||
android:nextFocusDown="@+id/inputUrl"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:transitionName="site_name"
|
|
||||||
tools:ignore="Autofill,UnusedAttribute"
|
|
||||||
style="@style/NockText.Body"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/inputUrl"
|
android:id="@+id/inputUrl"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -127,7 +128,7 @@
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/content_inset"
|
android:layout_marginTop="@dimen/content_inset_less"
|
||||||
android:text="@string/response_timeout"
|
android:text="@string/response_timeout"
|
||||||
style="@style/NockText.SectionHeader"
|
style="@style/NockText.SectionHeader"
|
||||||
/>
|
/>
|
||||||
|
@ -217,6 +218,16 @@
|
||||||
|
|
||||||
<include layout="@layout/include_divider"/>
|
<include layout="@layout/include_divider"/>
|
||||||
|
|
||||||
|
<com.afollestad.nocknock.viewcomponents.headers.HeaderStackLayout
|
||||||
|
android:id="@+id/headersLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/content_inset_half"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<include layout="@layout/include_divider"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -251,24 +262,6 @@
|
||||||
style="@style/NockText.Body"
|
style="@style/NockText.Body"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/doneBtn"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/content_inset_double"
|
|
||||||
android:text="@string/save_changes"
|
|
||||||
style="@style/AccentButton"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/disableChecksButton"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/content_inset_quarter"
|
|
||||||
android:text="@string/disable_automatic_checks"
|
|
||||||
style="@style/PrimaryDarkButton"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
9
app/src/main/res/menu/menu_addsite.xml
Normal file
9
app/src/main/res/menu/menu_addsite.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/commit"
|
||||||
|
android:icon="@drawable/ic_check"
|
||||||
|
android:title="@string/add_site"
|
||||||
|
app:showAsAction="ifRoom"/>
|
||||||
|
</menu>
|
|
@ -1,17 +1,23 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/commit"
|
||||||
|
android:icon="@drawable/ic_check"
|
||||||
|
android:title="@string/save_changes"
|
||||||
|
app:showAsAction="ifRoom"/>
|
||||||
<item
|
<item
|
||||||
android:id="@+id/refresh"
|
android:id="@+id/refresh"
|
||||||
android:icon="@drawable/ic_action_refresh"
|
android:icon="@drawable/ic_action_refresh"
|
||||||
android:title="@string/refresh_status"
|
android:title="@string/refresh_status"
|
||||||
app:showAsAction="ifRoom"/>
|
app:showAsAction="ifRoom"/>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/remove"
|
android:id="@+id/remove"
|
||||||
android:icon="@drawable/ic_action_delete"
|
android:icon="@drawable/ic_action_delete"
|
||||||
android:title="@string/remove_site"
|
android:title="@string/remove_site"
|
||||||
app:showAsAction="ifRoom"/>
|
app:showAsAction="ifRoom"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/disableChecks"
|
||||||
|
android:title="@string/disable_automatic_checks"
|
||||||
|
/>
|
||||||
</menu>
|
</menu>
|
||||||
|
|
|
@ -2,5 +2,6 @@
|
||||||
|
|
||||||
<dimen name="empty_text_size">28sp</dimen>
|
<dimen name="empty_text_size">28sp</dimen>
|
||||||
<dimen name="list_text_spacing">6dp</dimen>
|
<dimen name="list_text_spacing">6dp</dimen>
|
||||||
|
<dimen name="toolbar_elevation">4dp</dimen>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -20,9 +20,11 @@
|
||||||
<string name="dismiss">Dismiss</string>
|
<string name="dismiss">Dismiss</string>
|
||||||
<string name="add_site">Add Site</string>
|
<string name="add_site">Add Site</string>
|
||||||
<string name="site_name">Site Name</string>
|
<string name="site_name">Site Name</string>
|
||||||
|
<string name="site_name_hint">Site display name</string>
|
||||||
<string name="site_url">Site URL</string>
|
<string name="site_url">Site URL</string>
|
||||||
|
<string name="site_url_hint">https://yoursite.com</string>
|
||||||
<string name="site_tags">Site Tags</string>
|
<string name="site_tags">Site Tags</string>
|
||||||
<string name="site_tags_hint">Site Tags (one, two, three)</string>
|
<string name="site_tags_hint">One,Two,Three</string>
|
||||||
<string name="please_enter_name">Please enter a name!</string>
|
<string name="please_enter_name">Please enter a name!</string>
|
||||||
<string name="please_enter_url">Please enter a URL.</string>
|
<string name="please_enter_url">Please enter a URL.</string>
|
||||||
<string name="please_enter_valid_url">Please enter a valid URL.</string>
|
<string name="please_enter_valid_url">Please enter a valid URL.</string>
|
||||||
|
@ -46,8 +48,8 @@
|
||||||
<string name="disable_automatic_checks">Disable Automatic Validation</string>
|
<string name="disable_automatic_checks">Disable Automatic Validation</string>
|
||||||
<string name="disable_automatic_checks_prompt"><![CDATA[
|
<string name="disable_automatic_checks_prompt"><![CDATA[
|
||||||
Disable automatic validation for <b>%1$s</b>? The site will not be checked in the background
|
Disable automatic validation for <b>%1$s</b>? 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
|
until you re-enable validation by tapping the checkmark (Save) icon. You can still manually
|
||||||
Refresh icon at the top of this page.
|
perform validation by tapping the Refresh icon at the top of this page.
|
||||||
]]></string>
|
]]></string>
|
||||||
<string name="disable">Disable</string>
|
<string name="disable">Disable</string>
|
||||||
<string name="renable_and_save_changes">Enable Auto Validation & Save Changes</string>
|
<string name="renable_and_save_changes">Enable Auto Validation & Save Changes</string>
|
||||||
|
|
|
@ -16,4 +16,8 @@
|
||||||
<item name="android:fontFamily">@font/lato</item>
|
<item name="android:fontFamily">@font/lato</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="AccentTextButton" parent="Widget.MaterialComponents.Button.TextButton">
|
||||||
|
<item name="android:fontFamily">@font/lato</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -6,4 +6,28 @@
|
||||||
<item name="android:textColor">?toolbarTitleColor</item>
|
<item name="android:textColor">?toolbarTitleColor</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="InputForm"/>
|
||||||
|
|
||||||
|
<style name="InputForm.Header" parent="NockText.SectionHeader">
|
||||||
|
<item name="android:layout_width">wrap_content</item>
|
||||||
|
<item name="android:layout_height">wrap_content</item>
|
||||||
|
<item name="android:layout_marginTop">@dimen/content_inset_less</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="InputForm.Field" parent="NockText.Body">
|
||||||
|
<item name="android:layout_width">match_parent</item>
|
||||||
|
<item name="android:layout_height">wrap_content</item>
|
||||||
|
<item name="android:layout_marginTop">@dimen/content_inset_quarter</item>
|
||||||
|
<item name="android:singleLine">true</item>
|
||||||
|
<item name="android:imeOptions">actionNext</item>
|
||||||
|
<item name="android:layout_marginStart">-4dp</item>
|
||||||
|
<item name="android:layout_marginEnd">-4dp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="InputForm.FieldNote" parent="NockText.Footnote">
|
||||||
|
<item name="android:layout_width">wrap_content</item>
|
||||||
|
<item name="android:layout_height">wrap_content</item>
|
||||||
|
<item name="android:layout_marginTop">@dimen/list_text_spacing</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -24,6 +24,7 @@ import com.afollestad.nocknock.data.RetryPolicyDao
|
||||||
import com.afollestad.nocknock.data.SiteDao
|
import com.afollestad.nocknock.data.SiteDao
|
||||||
import com.afollestad.nocknock.data.SiteSettingsDao
|
import com.afollestad.nocknock.data.SiteSettingsDao
|
||||||
import com.afollestad.nocknock.data.ValidationResultsDao
|
import com.afollestad.nocknock.data.ValidationResultsDao
|
||||||
|
import com.afollestad.nocknock.data.model.Header
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.SiteSettings
|
import com.afollestad.nocknock.data.model.SiteSettings
|
||||||
import com.afollestad.nocknock.data.model.Status
|
import com.afollestad.nocknock.data.model.Status
|
||||||
|
@ -79,6 +80,13 @@ fun fakeRetryPolicy(
|
||||||
minutes = minutes
|
minutes = minutes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun fakeHeaders(siteId: Long): List<Header> {
|
||||||
|
return listOf(
|
||||||
|
Header(id = siteId + 1, siteId = siteId, key = "Content-Type", value = "text/html"),
|
||||||
|
Header(id = siteId + 2, siteId = siteId, key = "User-Agent", value = "NockNock")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun fakeModel(id: Long) = Site(
|
fun fakeModel(id: Long) = Site(
|
||||||
id = id,
|
id = id,
|
||||||
name = "Test",
|
name = "Test",
|
||||||
|
@ -86,7 +94,8 @@ fun fakeModel(id: Long) = Site(
|
||||||
tags = "",
|
tags = "",
|
||||||
settings = fakeSettingsModel(id),
|
settings = fakeSettingsModel(id),
|
||||||
lastResult = fakeResultModel(id),
|
lastResult = fakeResultModel(id),
|
||||||
retryPolicy = fakeRetryPolicy(id)
|
retryPolicy = fakeRetryPolicy(id),
|
||||||
|
headers = fakeHeaders(id)
|
||||||
)
|
)
|
||||||
|
|
||||||
val MOCK_MODEL_1 = fakeModel(1)
|
val MOCK_MODEL_1 = fakeModel(1)
|
||||||
|
|
|
@ -22,7 +22,7 @@ import com.afollestad.nocknock.data.model.SiteSettings
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||||
import com.afollestad.nocknock.engine.validation.ValidationManager
|
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||||
import com.afollestad.nocknock.mockDatabase
|
import com.afollestad.nocknock.mockDatabase
|
||||||
import com.afollestad.nocknock.utilities.ext.MINUTE
|
import com.afollestad.nocknock.utilities.ext.MINUTE
|
||||||
import com.afollestad.nocknock.utilities.livedata.test
|
import com.afollestad.nocknock.utilities.livedata.test
|
||||||
|
@ -44,7 +44,7 @@ import org.junit.Test
|
||||||
class AddSiteViewModelTest {
|
class AddSiteViewModelTest {
|
||||||
|
|
||||||
private val database = mockDatabase()
|
private val database = mockDatabase()
|
||||||
private val validationManager = mock<ValidationManager>()
|
private val validationManager = mock<ValidationExecutor>()
|
||||||
|
|
||||||
@Rule @JvmField val rule = InstantTaskExecutorRule()
|
@Rule @JvmField val rule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
@ -170,7 +170,7 @@ class AddSiteViewModelTest {
|
||||||
viewModel.commit(onDone)
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
verify(validationManager, never())
|
verify(validationManager, never())
|
||||||
.scheduleCheck(any(), any(), any(), any())
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
onNameError.assertValues(R.string.please_enter_name)
|
onNameError.assertValues(R.string.please_enter_name)
|
||||||
onUrlError.assertNoValues()
|
onUrlError.assertNoValues()
|
||||||
onTimeoutError.assertNoValues()
|
onTimeoutError.assertNoValues()
|
||||||
|
@ -202,7 +202,7 @@ class AddSiteViewModelTest {
|
||||||
viewModel.commit(onDone)
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
verify(validationManager, never())
|
verify(validationManager, never())
|
||||||
.scheduleCheck(any(), any(), any(), any())
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
onNameError.assertNoValues()
|
onNameError.assertNoValues()
|
||||||
onUrlError.assertValues(R.string.please_enter_url)
|
onUrlError.assertValues(R.string.please_enter_url)
|
||||||
onTimeoutError.assertNoValues()
|
onTimeoutError.assertNoValues()
|
||||||
|
@ -234,7 +234,7 @@ class AddSiteViewModelTest {
|
||||||
viewModel.commit(onDone)
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
verify(validationManager, never())
|
verify(validationManager, never())
|
||||||
.scheduleCheck(any(), any(), any(), any())
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
onNameError.assertNoValues()
|
onNameError.assertNoValues()
|
||||||
onUrlError.assertValues(R.string.please_enter_valid_url)
|
onUrlError.assertValues(R.string.please_enter_valid_url)
|
||||||
onTimeoutError.assertNoValues()
|
onTimeoutError.assertNoValues()
|
||||||
|
@ -266,7 +266,7 @@ class AddSiteViewModelTest {
|
||||||
viewModel.commit(onDone)
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
verify(validationManager, never())
|
verify(validationManager, never())
|
||||||
.scheduleCheck(any(), any(), any(), any())
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
onNameError.assertNoValues()
|
onNameError.assertNoValues()
|
||||||
onUrlError.assertNoValues()
|
onUrlError.assertNoValues()
|
||||||
onTimeoutError.assertValues(R.string.please_enter_networkTimeout)
|
onTimeoutError.assertValues(R.string.please_enter_networkTimeout)
|
||||||
|
@ -298,7 +298,7 @@ class AddSiteViewModelTest {
|
||||||
viewModel.commit(onDone)
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
verify(validationManager, never())
|
verify(validationManager, never())
|
||||||
.scheduleCheck(any(), any(), any(), any())
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
onNameError.assertNoValues()
|
onNameError.assertNoValues()
|
||||||
onUrlError.assertNoValues()
|
onUrlError.assertNoValues()
|
||||||
onTimeoutError.assertNoValues()
|
onTimeoutError.assertNoValues()
|
||||||
|
@ -331,7 +331,7 @@ class AddSiteViewModelTest {
|
||||||
viewModel.commit(onDone)
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
verify(validationManager, never())
|
verify(validationManager, never())
|
||||||
.scheduleCheck(any(), any(), any(), any())
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
onNameError.assertNoValues()
|
onNameError.assertNoValues()
|
||||||
onUrlError.assertNoValues()
|
onUrlError.assertNoValues()
|
||||||
onTimeoutError.assertNoValues()
|
onTimeoutError.assertNoValues()
|
||||||
|
@ -364,7 +364,7 @@ class AddSiteViewModelTest {
|
||||||
viewModel.commit(onDone)
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
verify(validationManager, never())
|
verify(validationManager, never())
|
||||||
.scheduleCheck(any(), any(), any(), any())
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
onNameError.assertNoValues()
|
onNameError.assertNoValues()
|
||||||
onUrlError.assertNoValues()
|
onUrlError.assertNoValues()
|
||||||
onTimeoutError.assertNoValues()
|
onTimeoutError.assertNoValues()
|
||||||
|
@ -410,7 +410,7 @@ class AddSiteViewModelTest {
|
||||||
lastResult = null
|
lastResult = null
|
||||||
)
|
)
|
||||||
|
|
||||||
verify(validationManager).scheduleCheck(
|
verify(validationManager).scheduleValidation(
|
||||||
site = model,
|
site = model,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true,
|
cancelPrevious = true,
|
||||||
|
|
|
@ -20,7 +20,7 @@ import com.afollestad.nocknock.ALL_MOCK_MODELS
|
||||||
import com.afollestad.nocknock.MOCK_MODEL_1
|
import com.afollestad.nocknock.MOCK_MODEL_1
|
||||||
import com.afollestad.nocknock.MOCK_MODEL_2
|
import com.afollestad.nocknock.MOCK_MODEL_2
|
||||||
import com.afollestad.nocknock.MOCK_MODEL_3
|
import com.afollestad.nocknock.MOCK_MODEL_3
|
||||||
import com.afollestad.nocknock.engine.validation.ValidationManager
|
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||||
import com.afollestad.nocknock.mockDatabase
|
import com.afollestad.nocknock.mockDatabase
|
||||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.utilities.livedata.test
|
import com.afollestad.nocknock.utilities.livedata.test
|
||||||
|
@ -39,7 +39,7 @@ class MainViewModelTest {
|
||||||
|
|
||||||
private val database = mockDatabase()
|
private val database = mockDatabase()
|
||||||
private val notificationManager = mock<NockNotificationManager>()
|
private val notificationManager = mock<NockNotificationManager>()
|
||||||
private val validationManager = mock<ValidationManager>()
|
private val validationManager = mock<ValidationExecutor>()
|
||||||
|
|
||||||
@Rule @JvmField val rule = InstantTaskExecutorRule()
|
@Rule @JvmField val rule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ class MainViewModelTest {
|
||||||
viewModel.onResume()
|
viewModel.onResume()
|
||||||
|
|
||||||
verify(notificationManager).cancelStatusNotifications()
|
verify(notificationManager).cancelStatusNotifications()
|
||||||
verify(validationManager).ensureScheduledChecks()
|
verify(validationManager).ensureScheduledValidations()
|
||||||
|
|
||||||
sites.assertValues(
|
sites.assertValues(
|
||||||
listOf(),
|
listOf(),
|
||||||
|
@ -106,7 +106,7 @@ class MainViewModelTest {
|
||||||
@Test fun refreshSite() {
|
@Test fun refreshSite() {
|
||||||
viewModel.refreshSite(MOCK_MODEL_3)
|
viewModel.refreshSite(MOCK_MODEL_3)
|
||||||
|
|
||||||
verify(validationManager).scheduleCheck(
|
verify(validationManager).scheduleValidation(
|
||||||
site = MOCK_MODEL_3,
|
site = MOCK_MODEL_3,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true
|
cancelPrevious = true
|
||||||
|
@ -132,7 +132,7 @@ class MainViewModelTest {
|
||||||
sites.assertNoValues()
|
sites.assertNoValues()
|
||||||
isLoading.assertValues(true, false)
|
isLoading.assertValues(true, false)
|
||||||
|
|
||||||
verify(validationManager).cancelCheck(modifiedModel)
|
verify(validationManager).cancelScheduledValidation(modifiedModel)
|
||||||
verify(notificationManager).cancelStatusNotification(modifiedModel)
|
verify(notificationManager).cancelStatusNotification(modifiedModel)
|
||||||
verify(database.siteDao()).delete(modifiedModel)
|
verify(database.siteDao()).delete(modifiedModel)
|
||||||
verify(database.siteSettingsDao()).delete(modifiedModel.settings!!)
|
verify(database.siteSettingsDao()).delete(modifiedModel.settings!!)
|
||||||
|
@ -163,7 +163,7 @@ class MainViewModelTest {
|
||||||
isLoading.assertValues(true, false)
|
isLoading.assertValues(true, false)
|
||||||
emptyTextVisibility.assertValues(false, false, false)
|
emptyTextVisibility.assertValues(false, false, false)
|
||||||
|
|
||||||
verify(validationManager).cancelCheck(MOCK_MODEL_1)
|
verify(validationManager).cancelScheduledValidation(MOCK_MODEL_1)
|
||||||
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
|
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
|
||||||
verify(database.siteDao()).delete(MOCK_MODEL_1)
|
verify(database.siteDao()).delete(MOCK_MODEL_1)
|
||||||
verify(database.siteSettingsDao()).delete(MOCK_MODEL_1.settings!!)
|
verify(database.siteSettingsDao()).delete(MOCK_MODEL_1.settings!!)
|
||||||
|
|
|
@ -28,7 +28,7 @@ import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
import com.afollestad.nocknock.data.model.ValidationMode.STATUS_CODE
|
||||||
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH
|
||||||
import com.afollestad.nocknock.data.model.ValidationResult
|
import com.afollestad.nocknock.data.model.ValidationResult
|
||||||
import com.afollestad.nocknock.engine.validation.ValidationManager
|
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||||
import com.afollestad.nocknock.mockDatabase
|
import com.afollestad.nocknock.mockDatabase
|
||||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.utilities.livedata.test
|
import com.afollestad.nocknock.utilities.livedata.test
|
||||||
|
@ -75,7 +75,7 @@ class ViewSiteViewModelTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val database = mockDatabase()
|
private val database = mockDatabase()
|
||||||
private val validationManager = mock<ValidationManager>()
|
private val validationManager = mock<ValidationExecutor>()
|
||||||
private val notificationManager = mock<NockNotificationManager>()
|
private val notificationManager = mock<NockNotificationManager>()
|
||||||
|
|
||||||
@Rule @JvmField val rule = InstantTaskExecutorRule()
|
@Rule @JvmField val rule = InstantTaskExecutorRule()
|
||||||
|
@ -276,7 +276,7 @@ class ViewSiteViewModelTest {
|
||||||
viewModel.commit(onDone)
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
verify(validationManager, never())
|
verify(validationManager, never())
|
||||||
.scheduleCheck(any(), any(), any(), any())
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
onNameError.assertValues(R.string.please_enter_name)
|
onNameError.assertValues(R.string.please_enter_name)
|
||||||
onUrlError.assertNoValues()
|
onUrlError.assertNoValues()
|
||||||
onTimeoutError.assertNoValues()
|
onTimeoutError.assertNoValues()
|
||||||
|
@ -308,7 +308,7 @@ class ViewSiteViewModelTest {
|
||||||
viewModel.commit(onDone)
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
verify(validationManager, never())
|
verify(validationManager, never())
|
||||||
.scheduleCheck(any(), any(), any(), any())
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
onNameError.assertNoValues()
|
onNameError.assertNoValues()
|
||||||
onUrlError.assertValues(R.string.please_enter_url)
|
onUrlError.assertValues(R.string.please_enter_url)
|
||||||
onTimeoutError.assertNoValues()
|
onTimeoutError.assertNoValues()
|
||||||
|
@ -340,7 +340,7 @@ class ViewSiteViewModelTest {
|
||||||
viewModel.commit(onDone)
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
verify(validationManager, never())
|
verify(validationManager, never())
|
||||||
.scheduleCheck(any(), any(), any(), any())
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
onNameError.assertNoValues()
|
onNameError.assertNoValues()
|
||||||
onUrlError.assertValues(R.string.please_enter_valid_url)
|
onUrlError.assertValues(R.string.please_enter_valid_url)
|
||||||
onTimeoutError.assertNoValues()
|
onTimeoutError.assertNoValues()
|
||||||
|
@ -372,7 +372,7 @@ class ViewSiteViewModelTest {
|
||||||
viewModel.commit(onDone)
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
verify(validationManager, never())
|
verify(validationManager, never())
|
||||||
.scheduleCheck(any(), any(), any(), any())
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
onNameError.assertNoValues()
|
onNameError.assertNoValues()
|
||||||
onUrlError.assertNoValues()
|
onUrlError.assertNoValues()
|
||||||
onTimeoutError.assertValues(R.string.please_enter_networkTimeout)
|
onTimeoutError.assertValues(R.string.please_enter_networkTimeout)
|
||||||
|
@ -404,7 +404,7 @@ class ViewSiteViewModelTest {
|
||||||
viewModel.commit(onDone)
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
verify(validationManager, never())
|
verify(validationManager, never())
|
||||||
.scheduleCheck(any(), any(), any(), any())
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
onNameError.assertNoValues()
|
onNameError.assertNoValues()
|
||||||
onUrlError.assertNoValues()
|
onUrlError.assertNoValues()
|
||||||
onTimeoutError.assertNoValues()
|
onTimeoutError.assertNoValues()
|
||||||
|
@ -437,7 +437,7 @@ class ViewSiteViewModelTest {
|
||||||
viewModel.commit(onDone)
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
verify(validationManager, never())
|
verify(validationManager, never())
|
||||||
.scheduleCheck(any(), any(), any(), any())
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
onNameError.assertNoValues()
|
onNameError.assertNoValues()
|
||||||
onUrlError.assertNoValues()
|
onUrlError.assertNoValues()
|
||||||
onTimeoutError.assertNoValues()
|
onTimeoutError.assertNoValues()
|
||||||
|
@ -470,7 +470,7 @@ class ViewSiteViewModelTest {
|
||||||
viewModel.commit(onDone)
|
viewModel.commit(onDone)
|
||||||
|
|
||||||
verify(validationManager, never())
|
verify(validationManager, never())
|
||||||
.scheduleCheck(any(), any(), any(), any())
|
.scheduleValidation(any(), any(), any(), any())
|
||||||
onNameError.assertNoValues()
|
onNameError.assertNoValues()
|
||||||
onUrlError.assertNoValues()
|
onUrlError.assertNoValues()
|
||||||
onTimeoutError.assertNoValues()
|
onTimeoutError.assertNoValues()
|
||||||
|
@ -534,7 +534,7 @@ class ViewSiteViewModelTest {
|
||||||
assertThat(settingsCaptor.firstValue).isEqualTo(updatedSettings)
|
assertThat(settingsCaptor.firstValue).isEqualTo(updatedSettings)
|
||||||
assertThat(resultCaptor.firstValue).isEqualTo(updatedResult)
|
assertThat(resultCaptor.firstValue).isEqualTo(updatedResult)
|
||||||
|
|
||||||
verify(validationManager).scheduleCheck(
|
verify(validationManager).scheduleValidation(
|
||||||
site = updatedModel,
|
site = updatedModel,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true,
|
cancelPrevious = true,
|
||||||
|
@ -562,7 +562,7 @@ class ViewSiteViewModelTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
viewModel.checkNow()
|
viewModel.checkNow()
|
||||||
verify(validationManager).scheduleCheck(
|
verify(validationManager).scheduleValidation(
|
||||||
site = expectedModel,
|
site = expectedModel,
|
||||||
rightNow = true,
|
rightNow = true,
|
||||||
cancelPrevious = true
|
cancelPrevious = true
|
||||||
|
@ -579,7 +579,7 @@ class ViewSiteViewModelTest {
|
||||||
viewModel.removeSite(onDone)
|
viewModel.removeSite(onDone)
|
||||||
isLoading.assertValues(true, false)
|
isLoading.assertValues(true, false)
|
||||||
|
|
||||||
verify(validationManager).cancelCheck(MOCK_MODEL_1)
|
verify(validationManager).cancelScheduledValidation(MOCK_MODEL_1)
|
||||||
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
|
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
|
||||||
verify(database.siteDao()).delete(MOCK_MODEL_1)
|
verify(database.siteDao()).delete(MOCK_MODEL_1)
|
||||||
verify(database.siteSettingsDao()).delete(MOCK_MODEL_1.settings!!)
|
verify(database.siteSettingsDao()).delete(MOCK_MODEL_1.settings!!)
|
||||||
|
@ -603,7 +603,7 @@ class ViewSiteViewModelTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
verify(validationManager).cancelCheck(MOCK_MODEL_1)
|
verify(validationManager).cancelScheduledValidation(MOCK_MODEL_1)
|
||||||
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
|
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
|
||||||
verify(database.siteDao()).update(expectedSite)
|
verify(database.siteDao()).update(expectedSite)
|
||||||
verify(database.siteSettingsDao()).update(expectedSite.settings!!)
|
verify(database.siteSettingsDao()).update(expectedSite.settings!!)
|
||||||
|
|
|
@ -21,6 +21,7 @@ import android.content.Context
|
||||||
import androidx.room.Room.inMemoryDatabaseBuilder
|
import androidx.room.Room.inMemoryDatabaseBuilder
|
||||||
import androidx.test.core.app.ApplicationProvider.getApplicationContext
|
import androidx.test.core.app.ApplicationProvider.getApplicationContext
|
||||||
import androidx.test.runner.AndroidJUnit4
|
import androidx.test.runner.AndroidJUnit4
|
||||||
|
import com.afollestad.nocknock.data.model.Header
|
||||||
import com.afollestad.nocknock.data.model.RetryPolicy
|
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.SiteSettings
|
import com.afollestad.nocknock.data.model.SiteSettings
|
||||||
|
@ -47,6 +48,7 @@ class AppDatabaseTest() {
|
||||||
private lateinit var settingsDao: SiteSettingsDao
|
private lateinit var settingsDao: SiteSettingsDao
|
||||||
private lateinit var resultsDao: ValidationResultsDao
|
private lateinit var resultsDao: ValidationResultsDao
|
||||||
private lateinit var retryDao: RetryPolicyDao
|
private lateinit var retryDao: RetryPolicyDao
|
||||||
|
private lateinit var headerDao: HeaderDao
|
||||||
|
|
||||||
@Before fun setup() {
|
@Before fun setup() {
|
||||||
val context = getApplicationContext<Context>()
|
val context = getApplicationContext<Context>()
|
||||||
|
@ -55,6 +57,7 @@ class AppDatabaseTest() {
|
||||||
settingsDao = db.siteSettingsDao()
|
settingsDao = db.siteSettingsDao()
|
||||||
resultsDao = db.validationResultsDao()
|
resultsDao = db.validationResultsDao()
|
||||||
retryDao = db.retryPolicyDao()
|
retryDao = db.retryPolicyDao()
|
||||||
|
headerDao = db.headerDao()
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
@ -70,7 +73,8 @@ class AppDatabaseTest() {
|
||||||
tags = "",
|
tags = "",
|
||||||
settings = null,
|
settings = null,
|
||||||
lastResult = null,
|
lastResult = null,
|
||||||
retryPolicy = null
|
retryPolicy = null,
|
||||||
|
headers = emptyList()
|
||||||
)
|
)
|
||||||
val newId1 = sitesDao.insert(model1)
|
val newId1 = sitesDao.insert(model1)
|
||||||
assertThat(newId1).isGreaterThan(0)
|
assertThat(newId1).isGreaterThan(0)
|
||||||
|
@ -81,7 +85,8 @@ class AppDatabaseTest() {
|
||||||
tags = "",
|
tags = "",
|
||||||
settings = null,
|
settings = null,
|
||||||
lastResult = null,
|
lastResult = null,
|
||||||
retryPolicy = null
|
retryPolicy = null,
|
||||||
|
headers = emptyList()
|
||||||
)
|
)
|
||||||
val newId2 = sitesDao.insert(model2)
|
val newId2 = sitesDao.insert(model2)
|
||||||
assertThat(newId2).isGreaterThan(newId1)
|
assertThat(newId2).isGreaterThan(newId1)
|
||||||
|
@ -99,7 +104,8 @@ class AppDatabaseTest() {
|
||||||
tags = "",
|
tags = "",
|
||||||
settings = null,
|
settings = null,
|
||||||
lastResult = null,
|
lastResult = null,
|
||||||
retryPolicy = null
|
retryPolicy = null,
|
||||||
|
headers = emptyList()
|
||||||
)
|
)
|
||||||
val newId = sitesDao.insert(model)
|
val newId = sitesDao.insert(model)
|
||||||
assertThat(newId).isGreaterThan(0)
|
assertThat(newId).isGreaterThan(0)
|
||||||
|
@ -115,7 +121,8 @@ class AppDatabaseTest() {
|
||||||
tags = "",
|
tags = "",
|
||||||
settings = null,
|
settings = null,
|
||||||
lastResult = null,
|
lastResult = null,
|
||||||
retryPolicy = null
|
retryPolicy = null,
|
||||||
|
headers = emptyList()
|
||||||
)
|
)
|
||||||
val newId = sitesDao.insert(initialModel)
|
val newId = sitesDao.insert(initialModel)
|
||||||
assertThat(newId).isGreaterThan(0)
|
assertThat(newId).isGreaterThan(0)
|
||||||
|
@ -140,7 +147,8 @@ class AppDatabaseTest() {
|
||||||
tags = "",
|
tags = "",
|
||||||
settings = null,
|
settings = null,
|
||||||
lastResult = null,
|
lastResult = null,
|
||||||
retryPolicy = null
|
retryPolicy = null,
|
||||||
|
headers = emptyList()
|
||||||
)
|
)
|
||||||
val newId1 = sitesDao.insert(model1)
|
val newId1 = sitesDao.insert(model1)
|
||||||
assertThat(newId1).isGreaterThan(0)
|
assertThat(newId1).isGreaterThan(0)
|
||||||
|
@ -151,7 +159,8 @@ class AppDatabaseTest() {
|
||||||
tags = "",
|
tags = "",
|
||||||
settings = null,
|
settings = null,
|
||||||
lastResult = null,
|
lastResult = null,
|
||||||
retryPolicy = null
|
retryPolicy = null,
|
||||||
|
headers = emptyList()
|
||||||
)
|
)
|
||||||
val newId2 = sitesDao.insert(model2)
|
val newId2 = sitesDao.insert(model2)
|
||||||
assertThat(newId2).isGreaterThan(newId1)
|
assertThat(newId2).isGreaterThan(newId1)
|
||||||
|
@ -338,6 +347,78 @@ class AppDatabaseTest() {
|
||||||
assertThat(retryDao.forSite(1)).isEmpty()
|
assertThat(retryDao.forSite(1)).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HeaderDao
|
||||||
|
|
||||||
|
@Test fun headers_insert_and_forSite() {
|
||||||
|
val models = listOf(
|
||||||
|
Header(
|
||||||
|
siteId = 1,
|
||||||
|
key = "Name",
|
||||||
|
value = "Aidan"
|
||||||
|
),
|
||||||
|
Header(
|
||||||
|
siteId = 1,
|
||||||
|
key = "Born",
|
||||||
|
value = "1995"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val newIds = headerDao.insert(models)
|
||||||
|
assertThat(newIds.first()).isEqualTo(1)
|
||||||
|
assertThat(newIds.last()).isEqualTo(2)
|
||||||
|
|
||||||
|
val finalModels = headerDao.forSite(1)
|
||||||
|
assertThat(finalModels.first()).isEqualTo(models.first().copy(id = 1))
|
||||||
|
assertThat(finalModels.last()).isEqualTo(models.last().copy(id = 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun headers_update() {
|
||||||
|
val models = listOf(
|
||||||
|
Header(
|
||||||
|
siteId = 1,
|
||||||
|
key = "Name",
|
||||||
|
value = "Aidan"
|
||||||
|
),
|
||||||
|
Header(
|
||||||
|
siteId = 1,
|
||||||
|
key = "Born",
|
||||||
|
value = "1995"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
headerDao.insert(models)
|
||||||
|
|
||||||
|
val insertedModel = headerDao.forSite(1)
|
||||||
|
.last()
|
||||||
|
val updatedModel = insertedModel.copy(
|
||||||
|
key = "Test",
|
||||||
|
value = "Hello"
|
||||||
|
)
|
||||||
|
assertThat(headerDao.update(updatedModel)).isEqualTo(1)
|
||||||
|
|
||||||
|
val finalModels = headerDao.forSite(1)
|
||||||
|
assertThat(finalModels.first()).isEqualTo(models.first().copy(id = 1))
|
||||||
|
assertThat(finalModels.last()).isEqualTo(updatedModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun headers_delete() {
|
||||||
|
val models = listOf(
|
||||||
|
Header(
|
||||||
|
siteId = 1,
|
||||||
|
key = "Name",
|
||||||
|
value = "Aidan"
|
||||||
|
),
|
||||||
|
Header(
|
||||||
|
siteId = 1,
|
||||||
|
key = "Born",
|
||||||
|
value = "1995"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
headerDao.insert(models)
|
||||||
|
|
||||||
|
val insertedModels = headerDao.forSite(1)
|
||||||
|
headerDao.delete(insertedModels)
|
||||||
|
assertThat(headerDao.forSite(1)).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
// Extension Methods
|
// Extension Methods
|
||||||
|
|
||||||
@Test fun extension_put_and_allSites() {
|
@Test fun extension_put_and_allSites() {
|
||||||
|
@ -352,25 +433,6 @@ class AppDatabaseTest() {
|
||||||
assertThat(allSites[2]).isEqualTo(MOCK_MODEL_3)
|
assertThat(allSites[2]).isEqualTo(MOCK_MODEL_3)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun extension_put_and_allSites_withTag() {
|
|
||||||
val model1 = MOCK_MODEL_1.copy(tags = "one,two,three")
|
|
||||||
val model2 = MOCK_MODEL_2.copy(tags = "four,five,six")
|
|
||||||
val model3 = MOCK_MODEL_3.copy(tags = "seven,eight,nine")
|
|
||||||
|
|
||||||
db.putSite(model1)
|
|
||||||
db.putSite(model2)
|
|
||||||
db.putSite(model3)
|
|
||||||
|
|
||||||
val allSites1 = db.allSites(forTag = "one")
|
|
||||||
assertThat(allSites1.single()).isEqualTo(model1)
|
|
||||||
|
|
||||||
val allSites2 = db.allSites(forTag = "five")
|
|
||||||
assertThat(allSites2.single()).isEqualTo(model2)
|
|
||||||
|
|
||||||
val allSites3 = db.allSites(forTag = "nine")
|
|
||||||
assertThat(allSites3.single()).isEqualTo(model3)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun extension_put_getSite() {
|
@Test fun extension_put_getSite() {
|
||||||
db.putSite(MOCK_MODEL_1)
|
db.putSite(MOCK_MODEL_1)
|
||||||
db.putSite(MOCK_MODEL_2)
|
db.putSite(MOCK_MODEL_2)
|
||||||
|
@ -403,12 +465,23 @@ class AppDatabaseTest() {
|
||||||
count = 4,
|
count = 4,
|
||||||
minutes = 8
|
minutes = 8
|
||||||
)
|
)
|
||||||
|
val updatedHeaders = listOf(
|
||||||
|
modelToUpdate.headers.first().copy(
|
||||||
|
key = "One",
|
||||||
|
value = "Hello"
|
||||||
|
),
|
||||||
|
modelToUpdate.headers.last().copy(
|
||||||
|
key = "Two",
|
||||||
|
value = "Hey"
|
||||||
|
)
|
||||||
|
)
|
||||||
val updatedModel = modelToUpdate.copy(
|
val updatedModel = modelToUpdate.copy(
|
||||||
name = "Oijrfouhef",
|
name = "Oijrfouhef",
|
||||||
url = "https://iojfdfsdk.io",
|
url = "https://iojfdfsdk.io",
|
||||||
settings = updatedSettings,
|
settings = updatedSettings,
|
||||||
lastResult = updatedValidationResult,
|
lastResult = updatedValidationResult,
|
||||||
retryPolicy = updatedRetryPolicy
|
retryPolicy = updatedRetryPolicy,
|
||||||
|
headers = updatedHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
db.updateSite(updatedModel)
|
db.updateSite(updatedModel)
|
||||||
|
@ -417,6 +490,8 @@ class AppDatabaseTest() {
|
||||||
assertThat(finalSite.settings).isEqualTo(updatedSettings)
|
assertThat(finalSite.settings).isEqualTo(updatedSettings)
|
||||||
assertThat(finalSite.lastResult).isEqualTo(updatedValidationResult)
|
assertThat(finalSite.lastResult).isEqualTo(updatedValidationResult)
|
||||||
assertThat(finalSite.retryPolicy).isEqualTo(updatedRetryPolicy)
|
assertThat(finalSite.retryPolicy).isEqualTo(updatedRetryPolicy)
|
||||||
|
assertThat(finalSite.headers.first()).isEqualTo(updatedHeaders.first())
|
||||||
|
assertThat(finalSite.headers.last()).isEqualTo(updatedHeaders.last())
|
||||||
assertThat(finalSite).isEqualTo(updatedModel)
|
assertThat(finalSite).isEqualTo(updatedModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -426,7 +501,7 @@ class AppDatabaseTest() {
|
||||||
db.putSite(MOCK_MODEL_3)
|
db.putSite(MOCK_MODEL_3)
|
||||||
val allSites = db.allSites()
|
val allSites = db.allSites()
|
||||||
|
|
||||||
db.deleteSite(MOCK_MODEL_2)
|
db.deleteSite(allSites[1])
|
||||||
|
|
||||||
val remainingSettings = settingsDao.all()
|
val remainingSettings = settingsDao.all()
|
||||||
assertThat(remainingSettings.size).isEqualTo(2)
|
assertThat(remainingSettings.size).isEqualTo(2)
|
||||||
|
@ -442,5 +517,12 @@ class AppDatabaseTest() {
|
||||||
assertThat(remainingRetryPolicies.size).isEqualTo(2)
|
assertThat(remainingRetryPolicies.size).isEqualTo(2)
|
||||||
assertThat(remainingRetryPolicies[0]).isEqualTo(allSites[0].retryPolicy!!)
|
assertThat(remainingRetryPolicies[0]).isEqualTo(allSites[0].retryPolicy!!)
|
||||||
assertThat(remainingRetryPolicies[1]).isEqualTo(allSites[2].retryPolicy!!)
|
assertThat(remainingRetryPolicies[1]).isEqualTo(allSites[2].retryPolicy!!)
|
||||||
|
|
||||||
|
val remainingHeaders = headerDao.all()
|
||||||
|
assertThat(remainingHeaders.size).isEqualTo(4)
|
||||||
|
assertThat(remainingHeaders[0]).isEqualTo(allSites[0].headers.first())
|
||||||
|
assertThat(remainingHeaders[1]).isEqualTo(allSites[0].headers.last())
|
||||||
|
assertThat(remainingHeaders[2]).isEqualTo(allSites[2].headers.first())
|
||||||
|
assertThat(remainingHeaders[3]).isEqualTo(allSites[2].headers.last())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.data
|
package com.afollestad.nocknock.data
|
||||||
|
|
||||||
|
import com.afollestad.nocknock.data.model.Header
|
||||||
import com.afollestad.nocknock.data.model.RetryPolicy
|
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.SiteSettings
|
import com.afollestad.nocknock.data.model.SiteSettings
|
||||||
|
@ -58,6 +59,11 @@ fun fakeRetryPolicy(
|
||||||
minutes = minutes
|
minutes = minutes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun fakeHeaders(siteId: Long) = listOf(
|
||||||
|
Header(siteId = siteId, key = "Content-Type", value = "text/html"),
|
||||||
|
Header(siteId = siteId, key = "User-Agent", value = "NockNock")
|
||||||
|
)
|
||||||
|
|
||||||
fun fakeModel(id: Long) = Site(
|
fun fakeModel(id: Long) = Site(
|
||||||
id = id,
|
id = id,
|
||||||
name = "Test",
|
name = "Test",
|
||||||
|
@ -65,7 +71,8 @@ fun fakeModel(id: Long) = Site(
|
||||||
tags = "",
|
tags = "",
|
||||||
settings = fakeSettingsModel(id),
|
settings = fakeSettingsModel(id),
|
||||||
lastResult = fakeResultModel(id),
|
lastResult = fakeResultModel(id),
|
||||||
retryPolicy = fakeRetryPolicy(id)
|
retryPolicy = fakeRetryPolicy(id),
|
||||||
|
headers = fakeHeaders(id)
|
||||||
)
|
)
|
||||||
|
|
||||||
val MOCK_MODEL_1 = fakeModel(1)
|
val MOCK_MODEL_1 = fakeModel(1)
|
||||||
|
|
|
@ -19,6 +19,7 @@ import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import com.afollestad.nocknock.data.model.Converters
|
import com.afollestad.nocknock.data.model.Converters
|
||||||
|
import com.afollestad.nocknock.data.model.Header
|
||||||
import com.afollestad.nocknock.data.model.RetryPolicy
|
import com.afollestad.nocknock.data.model.RetryPolicy
|
||||||
import com.afollestad.nocknock.data.model.Site
|
import com.afollestad.nocknock.data.model.Site
|
||||||
import com.afollestad.nocknock.data.model.SiteSettings
|
import com.afollestad.nocknock.data.model.SiteSettings
|
||||||
|
@ -27,12 +28,13 @@ import com.afollestad.nocknock.data.model.ValidationResult
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
|
Header::class,
|
||||||
RetryPolicy::class,
|
RetryPolicy::class,
|
||||||
ValidationResult::class,
|
ValidationResult::class,
|
||||||
SiteSettings::class,
|
SiteSettings::class,
|
||||||
Site::class
|
Site::class
|
||||||
],
|
],
|
||||||
version = 3,
|
version = 4,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
@ -45,6 +47,8 @@ abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun validationResultsDao(): ValidationResultsDao
|
abstract fun validationResultsDao(): ValidationResultsDao
|
||||||
|
|
||||||
abstract fun retryPolicyDao(): RetryPolicyDao
|
abstract fun retryPolicyDao(): RetryPolicyDao
|
||||||
|
|
||||||
|
abstract fun headerDao(): HeaderDao
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,10 +65,12 @@ fun AppDatabase.allSites(): List<Site> {
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
val retryPolicy = retryPolicyDao().forSite(it.id)
|
val retryPolicy = retryPolicyDao().forSite(it.id)
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
|
val headers = headerDao().forSite(it.id)
|
||||||
return@map it.copy(
|
return@map it.copy(
|
||||||
settings = settings,
|
settings = settings,
|
||||||
lastResult = lastResult,
|
lastResult = lastResult,
|
||||||
retryPolicy = retryPolicy
|
retryPolicy = retryPolicy,
|
||||||
|
headers = headers
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,10 +89,12 @@ fun AppDatabase.getSite(id: Long): Site? {
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
val retryPolicy = retryPolicyDao().forSite(id)
|
val retryPolicy = retryPolicyDao().forSite(id)
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
|
val headers = headerDao().forSite(id)
|
||||||
return result.copy(
|
return result.copy(
|
||||||
settings = settings,
|
settings = settings,
|
||||||
lastResult = lastResult,
|
lastResult = lastResult,
|
||||||
retryPolicy = retryPolicy
|
retryPolicy = retryPolicy,
|
||||||
|
headers = headers
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,14 +109,19 @@ fun AppDatabase.putSite(site: Site): Site {
|
||||||
val settingsWithSiteId = settings.copy(siteId = newId)
|
val settingsWithSiteId = settings.copy(siteId = newId)
|
||||||
val lastResultWithSiteId = site.lastResult?.copy(siteId = newId)
|
val lastResultWithSiteId = site.lastResult?.copy(siteId = newId)
|
||||||
val retryPolicyWithSiteId = site.retryPolicy?.copy(siteId = newId)
|
val retryPolicyWithSiteId = site.retryPolicy?.copy(siteId = newId)
|
||||||
siteSettingsDao().insert(settingsWithSiteId)
|
val headersWithSiteId = site.headers.map { it.copy(siteId = newId) }
|
||||||
|
|
||||||
|
siteSettingsDao().insert(settingsWithSiteId)
|
||||||
lastResultWithSiteId?.let { validationResultsDao().insert(it) }
|
lastResultWithSiteId?.let { validationResultsDao().insert(it) }
|
||||||
retryPolicyWithSiteId?.let { retryPolicyDao().insert(it) }
|
retryPolicyWithSiteId?.let { retryPolicyDao().insert(it) }
|
||||||
|
headerDao().insert(headersWithSiteId)
|
||||||
|
|
||||||
return site.copy(
|
return site.copy(
|
||||||
id = newId,
|
id = newId,
|
||||||
settings = settingsWithSiteId
|
settings = settingsWithSiteId,
|
||||||
|
lastResult = lastResultWithSiteId,
|
||||||
|
retryPolicy = retryPolicyWithSiteId,
|
||||||
|
headers = headersWithSiteId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,6 +165,13 @@ fun AppDatabase.updateSite(site: Site) {
|
||||||
retryPolicyDao().insert(retryPolicy)
|
retryPolicyDao().insert(retryPolicy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wipe existing headers
|
||||||
|
headerDao().delete(headerDao().forSite(site.id))
|
||||||
|
// Then add ones that still exist
|
||||||
|
site.headers.forEach { header ->
|
||||||
|
headerDao().insert(header.copy(id = 0, siteId = site.id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -163,5 +183,9 @@ fun AppDatabase.deleteSite(site: Site) {
|
||||||
site.settings?.let { siteSettingsDao().delete(it) }
|
site.settings?.let { siteSettingsDao().delete(it) }
|
||||||
site.lastResult?.let { validationResultsDao().delete(it) }
|
site.lastResult?.let { validationResultsDao().delete(it) }
|
||||||
site.retryPolicy?.let { retryPolicyDao().delete(it) }
|
site.retryPolicy?.let { retryPolicyDao().delete(it) }
|
||||||
|
if (site.headers.any { it.id == 0L }) {
|
||||||
|
throw IllegalStateException("Cannot delete header with ID = 0.")
|
||||||
|
}
|
||||||
|
headerDao().delete(site.headers)
|
||||||
siteDao().delete(site)
|
siteDao().delete(site)
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,3 +43,17 @@ class Database2to3Migration : Migration(2, 3) {
|
||||||
database.execSQL("ALTER TABLE `sites` ADD COLUMN tags TEXT NOT NULL DEFAULT ''")
|
database.execSQL("ALTER TABLE `sites` ADD COLUMN tags TEXT NOT NULL DEFAULT ''")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates the database from version 3 to 4.
|
||||||
|
*
|
||||||
|
* @author Aidan Follestad (@afollestad)
|
||||||
|
*/
|
||||||
|
class Database3to4Migration : Migration(3, 4) {
|
||||||
|
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL(
|
||||||
|
"CREATE TABLE `headers` (id INTEGER PRIMARY KEY NOT NULL, siteId INTEGER NOT NULL, `key` TEXT NOT NULL, value TEXT NOT NULL)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
47
data/src/main/java/com/afollestad/nocknock/data/HeaderDao.kt
Normal file
47
data/src/main/java/com/afollestad/nocknock/data/HeaderDao.kt
Normal file
|
@ -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.
|
||||||
|
*/
|
||||||
|
package com.afollestad.nocknock.data
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy.FAIL
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Update
|
||||||
|
import com.afollestad.nocknock.data.model.Header
|
||||||
|
|
||||||
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
|
@Dao
|
||||||
|
interface HeaderDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM headers ORDER BY siteId ASC")
|
||||||
|
fun all(): List<Header>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM headers WHERE siteId = :siteId")
|
||||||
|
fun forSite(siteId: Long): List<Header>
|
||||||
|
|
||||||
|
@Insert(onConflict = FAIL)
|
||||||
|
fun insert(headers: Header): Long
|
||||||
|
|
||||||
|
@Insert(onConflict = FAIL)
|
||||||
|
fun insert(headers: List<Header>): List<Long>
|
||||||
|
|
||||||
|
@Update(onConflict = FAIL)
|
||||||
|
fun update(header: Header): Int
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun delete(headers: List<Header>): Int
|
||||||
|
}
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package com.afollestad.nocknock.data.model
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an HTTP header that is sent with a site's validation attempts.
|
||||||
|
*
|
||||||
|
* @author Aidan Follestad (@afollestad)
|
||||||
|
*/
|
||||||
|
@Entity(tableName = "headers")
|
||||||
|
data class Header(
|
||||||
|
/** The header's unique datrabase ID. */
|
||||||
|
@PrimaryKey(autoGenerate = true) var id: Long = 0,
|
||||||
|
/** The [Site] this header belong to. */
|
||||||
|
var siteId: Long = 0,
|
||||||
|
/** The header key/name. */
|
||||||
|
var key: String = "",
|
||||||
|
/** The header value. */
|
||||||
|
var value: String = ""
|
||||||
|
) : Serializable {
|
||||||
|
|
||||||
|
constructor() : this(0, 0, "", "")
|
||||||
|
}
|
|
@ -40,10 +40,12 @@ data class Site(
|
||||||
/** The last validation attempt result for the site, if any. */
|
/** The last validation attempt result for the site, if any. */
|
||||||
@Ignore var lastResult: ValidationResult?,
|
@Ignore var lastResult: ValidationResult?,
|
||||||
/** The site's retry policy, if any. */
|
/** The site's retry policy, if any. */
|
||||||
@Ignore var retryPolicy: RetryPolicy?
|
@Ignore var retryPolicy: RetryPolicy?,
|
||||||
|
/** Request headers sent with this site's validation attempts. */
|
||||||
|
@Ignore var headers: List<Header>
|
||||||
) : CanNotifyModel {
|
) : CanNotifyModel {
|
||||||
|
|
||||||
constructor() : this(0, "", "", "", null, null, null)
|
constructor() : this(0, "", "", "", null, null, null, emptyList())
|
||||||
|
|
||||||
override fun notifyId(): Int = id.toInt()
|
override fun notifyId(): Int = id.toInt()
|
||||||
|
|
||||||
|
|
|
@ -15,14 +15,14 @@
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.engine
|
package com.afollestad.nocknock.engine
|
||||||
|
|
||||||
import com.afollestad.nocknock.engine.validation.RealValidationManager
|
import com.afollestad.nocknock.engine.validation.RealValidationExecutor
|
||||||
import com.afollestad.nocknock.engine.validation.ValidationManager
|
import com.afollestad.nocknock.engine.validation.ValidationExecutor
|
||||||
import org.koin.dsl.module.module
|
import org.koin.dsl.module.module
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
val engineModule = module {
|
val engineModule = module {
|
||||||
|
|
||||||
single {
|
single {
|
||||||
RealValidationManager(get(), get(), get(), get(), get(), get())
|
RealValidationExecutor(get(), get(), get(), get(), get(), get())
|
||||||
} bind ValidationManager::class
|
} bind ValidationExecutor::class
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ import timber.log.Timber.d as log
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
class BootReceiver : BroadcastReceiver(), KoinComponent {
|
class BootReceiver : BroadcastReceiver(), KoinComponent {
|
||||||
|
|
||||||
private val validationManager by inject<ValidationManager>()
|
private val validationManager by inject<ValidationExecutor>()
|
||||||
private val mainDispatcher by inject<CoroutineDispatcher>(name = MAIN_DISPATCHER)
|
private val mainDispatcher by inject<CoroutineDispatcher>(name = MAIN_DISPATCHER)
|
||||||
private val ioDispatcher by inject<CoroutineDispatcher>(name = IO_DISPATCHER)
|
private val ioDispatcher by inject<CoroutineDispatcher>(name = IO_DISPATCHER)
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ class BootReceiver : BroadcastReceiver(), KoinComponent {
|
||||||
|
|
||||||
val pendingResult = goAsync()
|
val pendingResult = goAsync()
|
||||||
GlobalScope.launch(mainDispatcher) {
|
GlobalScope.launch(mainDispatcher) {
|
||||||
withContext(ioDispatcher) { validationManager.ensureScheduledChecks() }
|
withContext(ioDispatcher) { validationManager.ensureScheduledValidations() }
|
||||||
pendingResult.resultCode = 0
|
pendingResult.resultCode = 0
|
||||||
pendingResult.finish()
|
pendingResult.finish()
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,11 +43,11 @@ data class CheckResult(
|
||||||
typealias ClientTimeoutChanger = (client: OkHttpClient, timeout: Int) -> OkHttpClient
|
typealias ClientTimeoutChanger = (client: OkHttpClient, timeout: Int) -> OkHttpClient
|
||||||
|
|
||||||
/** @author Aidan Follestad (@afollestad) */
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
interface ValidationManager {
|
interface ValidationExecutor {
|
||||||
|
|
||||||
suspend fun ensureScheduledChecks()
|
suspend fun ensureScheduledValidations()
|
||||||
|
|
||||||
fun scheduleCheck(
|
fun scheduleValidation(
|
||||||
site: Site,
|
site: Site,
|
||||||
rightNow: Boolean = false,
|
rightNow: Boolean = false,
|
||||||
cancelPrevious: Boolean = rightNow,
|
cancelPrevious: Boolean = rightNow,
|
||||||
|
@ -55,19 +55,19 @@ interface ValidationManager {
|
||||||
overrideDelay: Long = -1
|
overrideDelay: Long = -1
|
||||||
)
|
)
|
||||||
|
|
||||||
fun cancelCheck(site: Site)
|
fun cancelScheduledValidation(site: Site)
|
||||||
|
|
||||||
suspend fun performCheck(site: Site): CheckResult
|
suspend fun performValidation(site: Site): CheckResult
|
||||||
}
|
}
|
||||||
|
|
||||||
class RealValidationManager(
|
class RealValidationExecutor(
|
||||||
private val jobScheduler: JobScheduler,
|
private val jobScheduler: JobScheduler,
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val bundleProvider: BundleProvider,
|
private val bundleProvider: BundleProvider,
|
||||||
private val jobInfoProvider: JobInfoProvider,
|
private val jobInfoProvider: JobInfoProvider,
|
||||||
private val database: AppDatabase
|
private val database: AppDatabase
|
||||||
) : ValidationManager {
|
) : ValidationExecutor {
|
||||||
|
|
||||||
private var clientTimeoutChanger: ClientTimeoutChanger = { client, timeout ->
|
private var clientTimeoutChanger: ClientTimeoutChanger = { client, timeout ->
|
||||||
client.newBuilder()
|
client.newBuilder()
|
||||||
|
@ -75,37 +75,37 @@ class RealValidationManager(
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun ensureScheduledChecks() {
|
override suspend fun ensureScheduledValidations() {
|
||||||
val sites = database.allSites()
|
val sites = database.allSites()
|
||||||
if (sites.isEmpty()) {
|
if (sites.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log("Ensuring enabled sites have scheduled checks.")
|
log("Ensuring enabled sites have scheduled validations.")
|
||||||
sites.filter { it.settings?.disabled != true }
|
sites.filter { it.settings?.disabled != true }
|
||||||
.forEach { site ->
|
.forEach { site ->
|
||||||
val existingJob = jobForSite(site)
|
val existingJob = jobForSite(site)
|
||||||
if (existingJob == null) {
|
if (existingJob == null) {
|
||||||
log("Site ${site.id} does NOT have a scheduled job, running one now.")
|
log("Site ${site.id} does NOT have a scheduled job, running one now.")
|
||||||
scheduleCheck(site = site, rightNow = true)
|
scheduleValidation(site = site, rightNow = true)
|
||||||
} else {
|
} else {
|
||||||
log("Site ${site.id} already has a scheduled job. Nothing to do.")
|
log("Site ${site.id} already has a scheduled job. Nothing to do.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun scheduleCheck(
|
override fun scheduleValidation(
|
||||||
site: Site,
|
site: Site,
|
||||||
rightNow: Boolean,
|
rightNow: Boolean,
|
||||||
cancelPrevious: Boolean,
|
cancelPrevious: Boolean,
|
||||||
fromFinishingJob: Boolean,
|
fromFinishingJob: Boolean,
|
||||||
overrideDelay: Long
|
overrideDelay: Long
|
||||||
) {
|
) {
|
||||||
check(site.id != 0L) { "Cannot schedule checks for jobs with no ID." }
|
check(site.id != 0L) { "Cannot schedule validations for jobs with no ID." }
|
||||||
val siteSettings = site.settings
|
val siteSettings = site.settings
|
||||||
requireNotNull(siteSettings) { "Site settings must be populated." }
|
requireNotNull(siteSettings) { "Site settings must be populated." }
|
||||||
|
|
||||||
if (cancelPrevious) {
|
if (cancelPrevious) {
|
||||||
cancelCheck(site)
|
cancelScheduledValidation(site)
|
||||||
} else if (!fromFinishingJob) {
|
} else if (!fromFinishingJob) {
|
||||||
val existingJob = jobForSite(site)
|
val existingJob = jobForSite(site)
|
||||||
check(existingJob == null) {
|
check(existingJob == null) {
|
||||||
|
@ -113,7 +113,7 @@ class RealValidationManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Requesting a check job for site to be scheduled: $site")
|
log("Requesting a validation job for site to be scheduled: $site")
|
||||||
val extras = bundleProvider.createPersistable {
|
val extras = bundleProvider.createPersistable {
|
||||||
putLong(KEY_SITE_ID, site.id)
|
putLong(KEY_SITE_ID, site.id)
|
||||||
}
|
}
|
||||||
|
@ -131,28 +131,33 @@ class RealValidationManager(
|
||||||
|
|
||||||
val dispatchResult = jobScheduler.schedule(jobInfo)
|
val dispatchResult = jobScheduler.schedule(jobInfo)
|
||||||
if (dispatchResult != RESULT_SUCCESS) {
|
if (dispatchResult != RESULT_SUCCESS) {
|
||||||
log("Failed to schedule a check job for site: ${site.id}")
|
log("Failed to schedule a validation job for site: ${site.id}")
|
||||||
} else {
|
} else {
|
||||||
log("Check job successfully scheduled for site: ${site.id}")
|
log("Validation job successfully scheduled for site: ${site.id}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelCheck(site: Site) {
|
override fun cancelScheduledValidation(site: Site) {
|
||||||
check(site.id != 0L) { "Cannot cancel scheduled checks for jobs with no ID." }
|
check(site.id != 0L) { "Cannot cancel scheduled validations for jobs with no ID." }
|
||||||
log("Cancelling scheduled checks for site: ${site.id}")
|
log("Cancelling scheduled validations for site: ${site.id}")
|
||||||
jobScheduler.cancel(site.id.toInt())
|
jobScheduler.cancel(site.id.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun performCheck(site: Site): CheckResult {
|
override suspend fun performValidation(site: Site): CheckResult {
|
||||||
check(site.id != 0L) { "Cannot schedule checks for jobs with no ID." }
|
check(site.id != 0L) { "Cannot schedule validations for jobs with no ID." }
|
||||||
val siteSettings = site.settings
|
val siteSettings = site.settings
|
||||||
requireNotNull(siteSettings) { "Site settings must be populated." }
|
requireNotNull(siteSettings) { "Site settings must be populated." }
|
||||||
check(siteSettings.networkTimeout > 0) { "Network timeout not set for site ${site.id}" }
|
check(siteSettings.networkTimeout > 0) { "Network timeout not set for site ${site.id}" }
|
||||||
log("performCheck(${site.id}) - GET ${site.url}")
|
log("performValidation(${site.id}) - GET ${site.url}")
|
||||||
|
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(site.url)
|
.apply {
|
||||||
.get()
|
url(site.url)
|
||||||
|
get()
|
||||||
|
site.headers.forEach { header ->
|
||||||
|
addHeader(header.key, header.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
@ -161,13 +166,13 @@ class RealValidationManager(
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
if (response.isSuccessful || response.code() == 401) {
|
if (response.isSuccessful || response.code() == 401) {
|
||||||
log("performCheck(${site.id}) = Successful")
|
log("performValidation(${site.id}) = Successful")
|
||||||
CheckResult(
|
CheckResult(
|
||||||
model = site.withStatus(status = OK, reason = null),
|
model = site.withStatus(status = OK, reason = null),
|
||||||
response = response
|
response = response
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
log("performCheck(${site.id}) = Failure, HTTP code ${response.code()}")
|
log("performValidation(${site.id}) = Failure, HTTP code ${response.code()}")
|
||||||
CheckResult(
|
CheckResult(
|
||||||
model = site.withStatus(
|
model = site.withStatus(
|
||||||
status = ERROR,
|
status = ERROR,
|
||||||
|
@ -177,7 +182,7 @@ class RealValidationManager(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (timeoutEx: SocketTimeoutException) {
|
} catch (timeoutEx: SocketTimeoutException) {
|
||||||
log("performCheck(${site.id}) = Socket Timeout")
|
log("performValidation(${site.id}) = Socket Timeout")
|
||||||
CheckResult(
|
CheckResult(
|
||||||
model = site.withStatus(
|
model = site.withStatus(
|
||||||
status = ERROR,
|
status = ERROR,
|
||||||
|
@ -185,7 +190,7 @@ class RealValidationManager(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
log("performCheck(${site.id}) = Error: ${ex.message}")
|
log("performValidation(${site.id}) = Error: ${ex.message}")
|
||||||
CheckResult(model = site.withStatus(status = ERROR, reason = ex.message))
|
CheckResult(model = site.withStatus(status = ERROR, reason = ex.message))
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -60,7 +60,7 @@ class ValidationJob : JobService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private val database by inject<AppDatabase>()
|
private val database by inject<AppDatabase>()
|
||||||
private val validationManager by inject<ValidationManager>()
|
private val validationManager by inject<ValidationExecutor>()
|
||||||
private val notificationManager by inject<NockNotificationManager>()
|
private val notificationManager by inject<NockNotificationManager>()
|
||||||
|
|
||||||
override fun onStartJob(params: JobParameters): Boolean {
|
override fun onStartJob(params: JobParameters): Boolean {
|
||||||
|
@ -83,7 +83,7 @@ class ValidationJob : JobService() {
|
||||||
|
|
||||||
val jobResult = async(IO) {
|
val jobResult = async(IO) {
|
||||||
updateStatus(site, CHECKING)
|
updateStatus(site, CHECKING)
|
||||||
val checkResult = validationManager.performCheck(site)
|
val checkResult = validationManager.performValidation(site)
|
||||||
val resultModel = checkResult.model
|
val resultModel = checkResult.model
|
||||||
val resultResponse = checkResult.response
|
val resultResponse = checkResult.response
|
||||||
val result = resultModel.lastResult!!
|
val result = resultModel.lastResult!!
|
||||||
|
@ -153,7 +153,7 @@ class ValidationJob : JobService() {
|
||||||
updateTriesLeft(retryPolicy, retryPolicy.triesLeft)
|
updateTriesLeft(retryPolicy, retryPolicy.triesLeft)
|
||||||
|
|
||||||
val interval = retryPolicy.interval()
|
val interval = retryPolicy.interval()
|
||||||
validationManager.scheduleCheck(
|
validationManager.scheduleValidation(
|
||||||
site = jobResult,
|
site = jobResult,
|
||||||
fromFinishingJob = true,
|
fromFinishingJob = true,
|
||||||
overrideDelay = interval
|
overrideDelay = interval
|
||||||
|
@ -170,7 +170,7 @@ class ValidationJob : JobService() {
|
||||||
notificationManager.postStatusNotification(jobResult)
|
notificationManager.postStatusNotification(jobResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
validationManager.scheduleCheck(
|
validationManager.scheduleValidation(
|
||||||
site = jobResult,
|
site = jobResult,
|
||||||
fromFinishingJob = true
|
fromFinishingJob = true
|
||||||
)
|
)
|
||||||
|
|
|
@ -19,6 +19,7 @@ dependencies {
|
||||||
implementation project(':data')
|
implementation project(':data')
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:' + versions.androidxCore
|
implementation 'androidx.appcompat:appcompat:' + versions.androidxCore
|
||||||
|
implementation 'com.google.android.material:material:' + versions.googleMaterial
|
||||||
api 'androidx.lifecycle:lifecycle-extensions:' + versions.lifecycle
|
api 'androidx.lifecycle:lifecycle-extensions:' + versions.lifecycle
|
||||||
|
|
||||||
api 'com.squareup.okhttp3:okhttp:' + versions.okHttp
|
api 'com.squareup.okhttp3:okhttp:' + versions.okHttp
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* 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.viewcomponents.headers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import com.afollestad.nocknock.data.model.Header
|
||||||
|
import com.afollestad.nocknock.utilities.ext.onTextChanged
|
||||||
|
import com.afollestad.nocknock.viewcomponents.R
|
||||||
|
import kotlinx.android.synthetic.main.header_stack_item_content.view.inputKey
|
||||||
|
import kotlinx.android.synthetic.main.header_stack_item_content.view.inputValue
|
||||||
|
|
||||||
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
|
class HeaderItemLayout(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null
|
||||||
|
) : LinearLayout(context, attrs) {
|
||||||
|
|
||||||
|
private var header: Header? = null
|
||||||
|
private var stack: HeaderStackLayout? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
z
|
||||||
|
orientation = HORIZONTAL
|
||||||
|
inflate(context, R.layout.header_stack_item_content, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun attachHeader(
|
||||||
|
newHeader: Header,
|
||||||
|
parentStack: HeaderStackLayout
|
||||||
|
) {
|
||||||
|
this.header = newHeader
|
||||||
|
this.stack = parentStack
|
||||||
|
|
||||||
|
inputKey.run {
|
||||||
|
setText(newHeader.key)
|
||||||
|
onTextChanged {
|
||||||
|
header?.key = it.trim()
|
||||||
|
stack?.postLiveData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputValue.run {
|
||||||
|
setText(newHeader.value)
|
||||||
|
onTextChanged {
|
||||||
|
header?.value = it.trim()
|
||||||
|
stack?.postLiveData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
/**
|
||||||
|
* 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.viewcomponents.headers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.OnClickListener
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.afollestad.nocknock.data.model.Header
|
||||||
|
import com.afollestad.nocknock.viewcomponents.R
|
||||||
|
import kotlinx.android.synthetic.main.header_stack_item_content.view.btnRemove
|
||||||
|
import kotlinx.android.synthetic.main.header_stack_item_content.view.inputKey
|
||||||
|
import kotlinx.android.synthetic.main.header_stack_item_content.view.inputValue
|
||||||
|
import kotlinx.android.synthetic.main.header_stack_layout.view.addHeader
|
||||||
|
import kotlinx.android.synthetic.main.header_stack_layout.view.header_list as list
|
||||||
|
|
||||||
|
/** @author Aidan Follestad (@afollestad) */
|
||||||
|
class HeaderStackLayout(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null
|
||||||
|
) : LinearLayout(context, attrs), OnClickListener {
|
||||||
|
|
||||||
|
private var data: MutableLiveData<List<Header>>? = null
|
||||||
|
private var headers = mutableListOf<Header>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
orientation = VERTICAL
|
||||||
|
inflate(context, R.layout.header_stack_layout, this)
|
||||||
|
addHeader.setOnClickListener { addEntry(Header()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun attach(data: MutableLiveData<List<Header>>) {
|
||||||
|
list.removeAllViews()
|
||||||
|
headers.clear()
|
||||||
|
data.value?.forEach(::addEntry)
|
||||||
|
this.data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
fun postLiveData() = this.data?.postValue(headers)
|
||||||
|
|
||||||
|
override fun onClick(v: View) {
|
||||||
|
val index = v.tag as Int
|
||||||
|
list.removeViewAt(index)
|
||||||
|
headers.removeAt(index)
|
||||||
|
postLiveData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addEntry(forHeader: Header) {
|
||||||
|
// Keep track of reference for posting future changes.
|
||||||
|
headers.add(forHeader)
|
||||||
|
|
||||||
|
val li = LayoutInflater.from(context)
|
||||||
|
val entry = li.inflate(R.layout.header_stack_item, list, false) as HeaderItemLayout
|
||||||
|
list.addView(entry)
|
||||||
|
|
||||||
|
entry.run {
|
||||||
|
inputKey.setText(forHeader.key)
|
||||||
|
inputKey.post { entry.inputKey.requestFocus() }
|
||||||
|
attachHeader(forHeader, this@HeaderStackLayout)
|
||||||
|
inputValue.setText(forHeader.value)
|
||||||
|
|
||||||
|
btnRemove.tag = headers.size - 1
|
||||||
|
btnRemove.setOnClickListener(this@HeaderStackLayout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,8 +25,7 @@ import com.afollestad.nocknock.utilities.ext.DAY
|
||||||
import com.afollestad.nocknock.utilities.ext.HOUR
|
import com.afollestad.nocknock.utilities.ext.HOUR
|
||||||
import com.afollestad.nocknock.utilities.ext.MINUTE
|
import com.afollestad.nocknock.utilities.ext.MINUTE
|
||||||
import com.afollestad.nocknock.utilities.ext.WEEK
|
import com.afollestad.nocknock.utilities.ext.WEEK
|
||||||
import com.afollestad.nocknock.viewcomponents.R.array
|
import com.afollestad.nocknock.viewcomponents.R
|
||||||
import com.afollestad.nocknock.viewcomponents.R.layout
|
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.lifecycleOwner
|
import com.afollestad.nocknock.viewcomponents.livedata.lifecycleOwner
|
||||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
|
import com.afollestad.nocknock.viewcomponents.livedata.toViewError
|
||||||
|
@ -48,18 +47,18 @@ class ValidationIntervalLayout(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
orientation = VERTICAL
|
orientation = VERTICAL
|
||||||
inflate(context, layout.validation_interval_layout, this)
|
inflate(context, R.layout.validation_interval_layout, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinishInflate() {
|
override fun onFinishInflate() {
|
||||||
super.onFinishInflate()
|
super.onFinishInflate()
|
||||||
val spinnerAdapter = ArrayAdapter(
|
val spinnerAdapter = ArrayAdapter(
|
||||||
context,
|
context,
|
||||||
layout.list_item_spinner,
|
R.layout.list_item_spinner,
|
||||||
resources.getStringArray(array.interval_options)
|
resources.getStringArray(R.array.interval_options)
|
||||||
)
|
)
|
||||||
spinnerAdapter.setDropDownViewResource(
|
spinnerAdapter.setDropDownViewResource(
|
||||||
layout.list_item_spinner_dropdown
|
R.layout.list_item_spinner_dropdown
|
||||||
)
|
)
|
||||||
spinner.adapter = spinnerAdapter
|
spinner.adapter = spinnerAdapter
|
||||||
}
|
}
|
||||||
|
|
10
viewcomponents/src/main/res/drawable/ic_chevron_right.xml
Normal file
10
viewcomponents/src/main/res/drawable/ic_chevron_right.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="24.0"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:width="24dp">
|
||||||
|
<path
|
||||||
|
android:fillColor="?android:textColorPrimary"
|
||||||
|
android:pathData="M8.59,16.34l4.58,-4.59 -4.58,-4.59L10,5.75l6,6 -6,6z"/>
|
||||||
|
</vector>
|
10
viewcomponents/src/main/res/drawable/ic_close.xml
Normal file
10
viewcomponents/src/main/res/drawable/ic_close.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="24.0"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:width="24dp">
|
||||||
|
<path
|
||||||
|
android:fillColor="?android:textColorPrimary"
|
||||||
|
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||||
|
</vector>
|
6
viewcomponents/src/main/res/layout/header_stack_item.xml
Normal file
6
viewcomponents/src/main/res/layout/header_stack_item.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.afollestad.nocknock.viewcomponents.headers.HeaderItemLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
/>
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:parentTag="android.widget.LinearLayout"
|
||||||
|
>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/inputKey"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginEnd="@dimen/content_inset_quarter"
|
||||||
|
android:layout_marginStart="-4dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:hint="@string/header_name"
|
||||||
|
android:inputType="text"
|
||||||
|
android:nextFocusDown="@+id/inputValue"
|
||||||
|
android:nextFocusRight="@+id/inputValue"
|
||||||
|
android:singleLine="true"
|
||||||
|
tools:ignore="Autofill,UnusedAttribute"
|
||||||
|
style="@style/NockText.Body"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:focusable="false"
|
||||||
|
android:focusableInTouchMode="false"
|
||||||
|
android:src="@drawable/ic_chevron_right"
|
||||||
|
tools:ignore="ContentDescription"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/inputValue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="@dimen/content_inset_quarter"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:hint="@string/header_value"
|
||||||
|
android:inputType="text"
|
||||||
|
android:singleLine="true"
|
||||||
|
tools:ignore="Autofill,UnusedAttribute"
|
||||||
|
style="@style/NockText.Body"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
|
android:id="@+id/btnRemove"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="@dimen/content_inset_half"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
android:src="@drawable/ic_close"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</merge>
|
37
viewcomponents/src/main/res/layout/header_stack_layout.xml
Normal file
37
viewcomponents/src/main/res/layout/header_stack_layout.xml
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:parentTag="android.widget.LinearLayout"
|
||||||
|
>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/addHeader"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/add_header"
|
||||||
|
style="@style/AccentTextButton"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:lineSpacingMultiplier="1.2"
|
||||||
|
android:text="@string/header_desc"
|
||||||
|
style="@style/NockText.Body.Light"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/header_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/content_inset_half"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:ignore="UselessLeaf"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</merge>
|
|
@ -19,17 +19,25 @@
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:weightSum="2"
|
android:weightSum="2"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/check_interval_every"
|
||||||
|
style="@style/NockText.Body"
|
||||||
|
/>
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/input"
|
android:id="@+id/input"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="start|center_vertical"
|
android:layout_gravity="start|center_vertical"
|
||||||
android:layout_marginEnd="@dimen/content_inset_half"
|
android:layout_marginEnd="@dimen/content_inset_less"
|
||||||
android:layout_marginStart="-4dp"
|
android:layout_marginStart="@dimen/content_inset_less"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:hint="0"
|
android:hint="0"
|
||||||
android:inputType="number"
|
android:inputType="number"
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
<string name="function_end">}</string>
|
<string name="function_end">}</string>
|
||||||
|
|
||||||
<string name="check_interval">Check Interval</string>
|
<string name="check_interval">Check Interval</string>
|
||||||
|
<string name="check_interval_every">Every</string>
|
||||||
|
|
||||||
<string name="retry_policy">Retry Policy</string>
|
<string name="retry_policy">Retry Policy</string>
|
||||||
<string name="retry_policy_retry">Retry</string>
|
<string name="retry_policy_retry">Retry</string>
|
||||||
|
@ -16,4 +17,10 @@
|
||||||
values. After retrying %1$d times over %2$d minutes with no success, you will get a notification.
|
values. After retrying %1$d times over %2$d minutes with no success, you will get a notification.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
|
<string name="headers">Headers</string>
|
||||||
|
<string name="add_header">Add Header…</string>
|
||||||
|
<string name="header_desc">Add HTTP headers to each request made to validate this site.</string>
|
||||||
|
<string name="header_name">Header Name</string>
|
||||||
|
<string name="header_value">Header Value</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -9,6 +9,12 @@
|
||||||
<item name="android:textColor">?colorAccent</item>
|
<item name="android:textColor">?colorAccent</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="NockText.Header">
|
||||||
|
<item name="android:textSize">@dimen/title_font_size</item>
|
||||||
|
<item name="android:fontFamily">@font/lato_bold</item>
|
||||||
|
<item name="android:textColor">?android:textColorPrimary</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="NockText.Title">
|
<style name="NockText.Title">
|
||||||
<item name="android:textSize">@dimen/title_font_size</item>
|
<item name="android:textSize">@dimen/title_font_size</item>
|
||||||
<item name="android:fontFamily">@font/lato</item>
|
<item name="android:fontFamily">@font/lato</item>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue