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