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:
Aidan Follestad 2019-01-08 16:14:08 -08:00
commit 646bc25232
43 changed files with 846 additions and 278 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -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 &amp; Save Changes</string> <string name="renable_and_save_changes">Enable Auto Validation &amp; Save Changes</string>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -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, "", "")
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View 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"
/>

View file

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

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

View file

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

View file

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

View file

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