mirror of
https://github.com/afollestad/nock-nock.git
synced 2025-04-20 03:25:14 +00:00
Add data model and UI for retry policy, part of #30
This commit is contained in:
parent
09352724ee
commit
da7623db79
29 changed files with 538 additions and 54 deletions
|
@ -37,7 +37,7 @@ class StatusUpdateIntentReceiver(
|
|||
private var callback: SiteCallback?
|
||||
) : LifecycleObserver {
|
||||
|
||||
private val intentReceiver = object : BroadcastReceiver() {
|
||||
internal val intentReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
intent: Intent
|
||||
|
|
|
@ -22,6 +22,7 @@ import android.content.Context.JOB_SCHEDULER_SERVICE
|
|||
import android.content.Context.NOTIFICATION_SERVICE
|
||||
import androidx.room.Room.databaseBuilder
|
||||
import com.afollestad.nocknock.data.AppDatabase
|
||||
import com.afollestad.nocknock.data.Database1to2Migration
|
||||
import com.afollestad.nocknock.notifications.Qualifiers.MAIN_ACTIVITY_CLASS
|
||||
import com.afollestad.nocknock.ui.main.MainActivity
|
||||
import com.afollestad.nocknock.utilities.ext.systemService
|
||||
|
@ -35,7 +36,11 @@ val mainModule = module {
|
|||
|
||||
single(name = MAIN_ACTIVITY_CLASS) { mainActivityCls }
|
||||
|
||||
single { databaseBuilder(get(), AppDatabase::class.java, "NockNock.db").build() }
|
||||
single {
|
||||
databaseBuilder(get(), AppDatabase::class.java, "NockNock.db")
|
||||
.addMigrations(Database1to2Migration())
|
||||
.build()
|
||||
}
|
||||
|
||||
single {
|
||||
OkHttpClient.Builder()
|
||||
|
|
|
@ -19,6 +19,7 @@ import androidx.lifecycle.ViewModel
|
|||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
abstract class ScopedViewModel(mainDispatcher: CoroutineDispatcher) : ViewModel() {
|
||||
|
@ -31,5 +32,5 @@ abstract class ScopedViewModel(mainDispatcher: CoroutineDispatcher) : ViewModel(
|
|||
job.cancel()
|
||||
}
|
||||
|
||||
//@TestOnly open fun destroy() = job.cancel()
|
||||
@TestOnly open fun destroy() = job.cancel()
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ import kotlinx.android.synthetic.main.activity_addsite.loadingProgress
|
|||
import kotlinx.android.synthetic.main.activity_addsite.responseTimeoutInput
|
||||
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.textUrlWarning
|
||||
import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
|
||||
|
@ -108,6 +109,12 @@ class AddSiteActivity : DarkModeSwitchActivity() {
|
|||
errorData = viewModel.onCheckIntervalError()
|
||||
)
|
||||
|
||||
// Retry Policy
|
||||
retryPolicyLayout.attach(
|
||||
timesData = viewModel.retryPolicyTimes,
|
||||
minutesData = viewModel.retryPolicyMinutes
|
||||
)
|
||||
|
||||
// Done button
|
||||
doneBtn.setOnClickListener {
|
||||
viewModel.commit {
|
||||
|
|
|
@ -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.RetryPolicy
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.SiteSettings
|
||||
import com.afollestad.nocknock.data.model.ValidationMode
|
||||
|
@ -59,6 +60,8 @@ class AddSiteViewModel(
|
|||
val validationScript = MutableLiveData<String>()
|
||||
val checkIntervalValue = MutableLiveData<Int>()
|
||||
val checkIntervalUnit = MutableLiveData<Long>()
|
||||
val retryPolicyTimes = MutableLiveData<Int>()
|
||||
val retryPolicyMinutes = MutableLiveData<Int>()
|
||||
|
||||
@OnLifecycleEvent(ON_START)
|
||||
fun setDefaults() {
|
||||
|
@ -66,6 +69,8 @@ class AddSiteViewModel(
|
|||
validationMode.value = STATUS_CODE
|
||||
checkIntervalValue.value = 0
|
||||
checkIntervalUnit.value = MINUTE
|
||||
retryPolicyMinutes.value = 0
|
||||
retryPolicyMinutes.value = 0
|
||||
}
|
||||
|
||||
// Private properties
|
||||
|
@ -223,12 +228,22 @@ class AddSiteViewModel(
|
|||
networkTimeout = timeout.value!!,
|
||||
disabled = false
|
||||
)
|
||||
|
||||
val retryPolicyTimes = retryPolicyTimes.value ?: 0
|
||||
val retryPolicyMinutes = retryPolicyMinutes.value ?: 0
|
||||
val retryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) {
|
||||
RetryPolicy(count = retryPolicyTimes, minutes = retryPolicyMinutes)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return Site(
|
||||
id = 0,
|
||||
name = name.value!!.trim(),
|
||||
url = url.value!!.trim(),
|
||||
settings = newSettings,
|
||||
lastResult = null
|
||||
lastResult = null,
|
||||
retryPolicy = retryPolicy
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ import com.afollestad.nocknock.utilities.providers.IntentProvider
|
|||
import com.afollestad.nocknock.utilities.ui.toast
|
||||
import com.afollestad.nocknock.viewUrl
|
||||
import com.afollestad.nocknock.viewUrlWithApp
|
||||
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility
|
||||
import kotlinx.android.synthetic.main.activity_main.fab
|
||||
import kotlinx.android.synthetic.main.activity_main.list
|
||||
import kotlinx.android.synthetic.main.activity_main.loadingProgress
|
||||
|
@ -73,10 +73,9 @@ class MainActivity : DarkModeSwitchActivity() {
|
|||
}
|
||||
|
||||
viewModel.onSites()
|
||||
.observe(this, Observer {
|
||||
siteAdapter.set(it)
|
||||
emptyText.showOrHide(it.isEmpty())
|
||||
})
|
||||
.observe(this, Observer { siteAdapter.set(it) })
|
||||
viewModel.onEmptyTextVisibility()
|
||||
.toViewVisibility(this, emptyText)
|
||||
loadingProgress.observe(this, viewModel.onIsLoading())
|
||||
|
||||
processIntent(intent)
|
||||
|
|
|
@ -43,11 +43,14 @@ class MainViewModel(
|
|||
|
||||
private val sites = MutableLiveData<List<Site>>()
|
||||
private val isLoading = MutableLiveData<Boolean>()
|
||||
private val emptyTextVisibility = MutableLiveData<Boolean>()
|
||||
|
||||
@CheckResult fun onSites(): LiveData<List<Site>> = sites
|
||||
|
||||
@CheckResult fun onIsLoading(): LiveData<Boolean> = isLoading
|
||||
|
||||
@CheckResult fun onEmptyTextVisibility(): LiveData<Boolean> = emptyTextVisibility
|
||||
|
||||
@OnLifecycleEvent(ON_RESUME)
|
||||
fun onResume() = loadSites()
|
||||
|
||||
|
@ -76,15 +79,18 @@ class MainViewModel(
|
|||
scope.launch {
|
||||
isLoading.value = true
|
||||
withContext(ioDispatcher) { database.deleteSite(model) }
|
||||
|
||||
val currentSites = sites.value ?: return@launch
|
||||
val index = currentSites.indexOfFirst { it.id == model.id }
|
||||
isLoading.value = false
|
||||
if (index == -1) return@launch
|
||||
|
||||
sites.value = currentSites.toMutableList()
|
||||
val newSitesList = currentSites.toMutableList()
|
||||
.apply {
|
||||
removeAt(index)
|
||||
}
|
||||
sites.value = newSitesList
|
||||
emptyTextVisibility.value = newSitesList.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,6 +98,7 @@ class MainViewModel(
|
|||
scope.launch {
|
||||
notificationManager.cancelStatusNotifications()
|
||||
sites.value = listOf()
|
||||
emptyTextVisibility.value = false
|
||||
isLoading.value = true
|
||||
|
||||
val result = withContext(ioDispatcher) {
|
||||
|
@ -101,6 +108,7 @@ class MainViewModel(
|
|||
sites.value = result
|
||||
ensureCheckJobs()
|
||||
isLoading.value = false
|
||||
emptyTextVisibility.value = result.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ import kotlinx.android.synthetic.main.activity_viewsite.loadingProgress
|
|||
import kotlinx.android.synthetic.main.activity_viewsite.responseTimeoutInput
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.responseValidationMode
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.responseValidationSearchTerm
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.retryPolicyLayout
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.scriptInputLayout
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.scrollView
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.textLastCheckResult
|
||||
|
@ -137,6 +138,12 @@ class ViewSiteActivity : DarkModeSwitchActivity() {
|
|||
errorData = viewModel.onCheckIntervalError()
|
||||
)
|
||||
|
||||
// Retry Policy
|
||||
retryPolicyLayout.attach(
|
||||
timesData = viewModel.retryPolicyTimes,
|
||||
minutesData = viewModel.retryPolicyMinutes
|
||||
)
|
||||
|
||||
// Last/next check
|
||||
viewModel.onLastCheckResultText()
|
||||
.toViewText(this, textLastCheckResult)
|
||||
|
|
|
@ -23,6 +23,7 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.AppDatabase
|
||||
import com.afollestad.nocknock.data.RetryPolicy
|
||||
import com.afollestad.nocknock.data.deleteSite
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.Status
|
||||
|
@ -70,6 +71,8 @@ class ViewSiteViewModel(
|
|||
val validationScript = MutableLiveData<String>()
|
||||
val checkIntervalValue = MutableLiveData<Int>()
|
||||
val checkIntervalUnit = MutableLiveData<Long>()
|
||||
val retryPolicyTimes = MutableLiveData<Int>()
|
||||
val retryPolicyMinutes = MutableLiveData<Int>()
|
||||
internal val disabled = MutableLiveData<Boolean>()
|
||||
internal val lastResult = MutableLiveData<ValidationResult?>()
|
||||
|
||||
|
@ -309,10 +312,27 @@ class ViewSiteViewModel(
|
|||
networkTimeout = timeout.value!!,
|
||||
disabled = false
|
||||
)
|
||||
|
||||
val retryPolicyTimes = retryPolicyTimes.value ?: 0
|
||||
val retryPolicyMinutes = retryPolicyMinutes.value ?: 0
|
||||
val retryPolicy: RetryPolicy? = if (retryPolicyTimes > 0 && retryPolicyMinutes > 0) {
|
||||
if (site.retryPolicy != null) {
|
||||
// Have existing policy, update it
|
||||
site.retryPolicy!!.copy(count = retryPolicyTimes, minutes = retryPolicyMinutes)
|
||||
} else {
|
||||
// Create new policy
|
||||
RetryPolicy(count = retryPolicyTimes, minutes = retryPolicyMinutes)
|
||||
}
|
||||
} else {
|
||||
// No policy
|
||||
null
|
||||
}
|
||||
|
||||
return site.copy(
|
||||
name = name.value!!.trim(),
|
||||
url = url.value!!.trim(),
|
||||
settings = newSettings
|
||||
settings = newSettings,
|
||||
retryPolicy = retryPolicy
|
||||
)
|
||||
.withStatus(status = WAITING)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.afollestad.nocknock.ui.viewsite
|
||||
|
||||
import com.afollestad.nocknock.data.RetryPolicy
|
||||
import com.afollestad.nocknock.data.model.Site
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
import com.afollestad.nocknock.data.model.ValidationMode.JAVASCRIPT
|
||||
|
@ -26,11 +27,8 @@ import com.afollestad.nocknock.utilities.ext.WEEK
|
|||
import kotlin.math.ceil
|
||||
|
||||
fun ViewSiteViewModel.setModel(site: Site) {
|
||||
requireNotNull(site.settings) {
|
||||
"Settings must be populated!"
|
||||
}
|
||||
val settings = site.settings ?: throw IllegalArgumentException("Settings must be populated!")
|
||||
this.site = site
|
||||
val settings = site.settings!!
|
||||
|
||||
status.value = site.lastResult?.status ?: WAITING
|
||||
name.value = site.name
|
||||
|
@ -54,6 +52,7 @@ fun ViewSiteViewModel.setModel(site: Site) {
|
|||
}
|
||||
|
||||
setCheckInterval(settings.validationIntervalMs)
|
||||
setRetryPolicy(site.retryPolicy)
|
||||
|
||||
this.disabled.value = settings.disabled
|
||||
this.lastResult.value = site.lastResult
|
||||
|
@ -88,6 +87,12 @@ private fun ViewSiteViewModel.setCheckInterval(interval: Long) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun ViewSiteViewModel.setRetryPolicy(policy: RetryPolicy?) {
|
||||
if (policy == null) return
|
||||
retryPolicyTimes.value = policy.count
|
||||
retryPolicyMinutes.value = policy.minutes
|
||||
}
|
||||
|
||||
private fun getIntervalFromUnit(
|
||||
millis: Long,
|
||||
unit: Long
|
||||
|
|
|
@ -102,6 +102,7 @@
|
|||
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"
|
||||
|
@ -109,11 +110,18 @@
|
|||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.retrypolicy.RetryPolicyLayout
|
||||
android:id="@+id/retryPolicyLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset_more"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/responseValidationLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
android:layout_marginTop="@dimen/content_inset_more"
|
||||
android:text="@string/response_validation_mode"
|
||||
style="@style/NockText.SectionHeader"
|
||||
/>
|
||||
|
|
|
@ -125,11 +125,19 @@
|
|||
android:layout_marginStart="-4dp"
|
||||
android:hint="@string/response_timeout_default"
|
||||
android:inputType="number"
|
||||
android:layout_marginTop="@dimen/content_inset_quarter"
|
||||
android:maxLength="8"
|
||||
tools:ignore="Autofill,HardcodedText,LabelFor"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<com.afollestad.nocknock.viewcomponents.retrypolicy.RetryPolicyLayout
|
||||
android:id="@+id/retryPolicyLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset"
|
||||
/>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
|
@ -234,7 +242,7 @@
|
|||
android:id="@+id/disableChecksButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset_half_quarter"
|
||||
android:layout_marginTop="@dimen/content_inset_quarter"
|
||||
android:text="@string/disable_automatic_checks"
|
||||
style="@style/PrimaryDarkButton"
|
||||
/>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<string name="about">About</string>
|
||||
<string name="about_body"><![CDATA[
|
||||
<b>Nock Nock</b>, a simple app designed by <b>Aidan Follestad</b>.<br/>
|
||||
<a href=\'https://aidanfollestad.com\'>Website</a>
|
||||
<a href=\'https://af.codes\'>Website</a>
|
||||
<a href=\'https://twitter.com/afollestad\'>Twitter</a>
|
||||
<a href=\'https://github.com/afollestad\'>GitHub</a>
|
||||
<a href=\'https://www.linkedin.com/in/afollestad\'>LinkedIn</a>
|
||||
|
|
|
@ -19,6 +19,8 @@ import android.app.PendingIntent
|
|||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import com.afollestad.nocknock.data.AppDatabase
|
||||
import com.afollestad.nocknock.data.RetryPolicy
|
||||
import com.afollestad.nocknock.data.RetryPolicyDao
|
||||
import com.afollestad.nocknock.data.SiteDao
|
||||
import com.afollestad.nocknock.data.SiteSettingsDao
|
||||
import com.afollestad.nocknock.data.ValidationResultsDao
|
||||
|
@ -67,12 +69,23 @@ fun fakeResultModel(
|
|||
timestampMs = currentTimeMillis()
|
||||
)
|
||||
|
||||
fun fakeRetryPolicy(
|
||||
id: Long,
|
||||
count: Int = 3,
|
||||
minutes: Int = 6
|
||||
) = RetryPolicy(
|
||||
siteId = id,
|
||||
count = count,
|
||||
minutes = minutes
|
||||
)
|
||||
|
||||
fun fakeModel(id: Long) = Site(
|
||||
id = id,
|
||||
name = "Test",
|
||||
url = "https://test.com",
|
||||
settings = fakeSettingsModel(id),
|
||||
lastResult = fakeResultModel(id)
|
||||
lastResult = fakeResultModel(id),
|
||||
retryPolicy = fakeRetryPolicy(id)
|
||||
)
|
||||
|
||||
val MOCK_MODEL_1 = fakeModel(1)
|
||||
|
@ -128,11 +141,26 @@ fun mockDatabase(): AppDatabase {
|
|||
on { update(isA()) } doReturn 1
|
||||
on { delete(isA()) } doReturn 1
|
||||
}
|
||||
val retryDao = mock<RetryPolicyDao> {
|
||||
on { insert(isA()) } doReturn 1L
|
||||
on { forSite(isA()) } doAnswer { inv ->
|
||||
val id = inv.getArgument<Long>(0)
|
||||
return@doAnswer when (id) {
|
||||
1L -> listOf(MOCK_MODEL_1.retryPolicy!!)
|
||||
2L -> listOf(MOCK_MODEL_2.retryPolicy!!)
|
||||
3L -> listOf(MOCK_MODEL_3.retryPolicy!!)
|
||||
else -> listOf()
|
||||
}
|
||||
}
|
||||
on { update(isA()) } doReturn 1
|
||||
on { delete(isA()) } doReturn 1
|
||||
}
|
||||
|
||||
return mock {
|
||||
on { siteDao() } doReturn siteDao
|
||||
on { siteSettingsDao() } doReturn settingsDao
|
||||
on { validationResultsDao() } doReturn resultsDao
|
||||
on { retryPolicyDao() } doReturn retryDao
|
||||
}
|
||||
}
|
||||
|
|
@ -56,6 +56,8 @@ class MainViewModelTest {
|
|||
@Test fun onResume() = runBlocking {
|
||||
val isLoading = viewModel.onIsLoading()
|
||||
.test()
|
||||
val emptyTextVisibility = viewModel.onEmptyTextVisibility()
|
||||
.test()
|
||||
val sites = viewModel.onSites()
|
||||
.test()
|
||||
|
||||
|
@ -69,6 +71,7 @@ class MainViewModelTest {
|
|||
ALL_MOCK_MODELS
|
||||
)
|
||||
isLoading.assertValues(true, false)
|
||||
emptyTextVisibility.assertValues(false, false)
|
||||
}
|
||||
|
||||
@Test fun postSiteUpdate_notFound() {
|
||||
|
@ -138,6 +141,8 @@ class MainViewModelTest {
|
|||
@Test fun removeSite() {
|
||||
val sites = viewModel.onSites()
|
||||
.test()
|
||||
val emptyTextVisibility = viewModel.onEmptyTextVisibility()
|
||||
.test()
|
||||
val isLoading = viewModel.onIsLoading()
|
||||
.test()
|
||||
|
||||
|
@ -156,6 +161,7 @@ class MainViewModelTest {
|
|||
|
||||
sites.assertValues(modelsWithout1)
|
||||
isLoading.assertValues(true, false)
|
||||
emptyTextVisibility.assertValues(false, false, false)
|
||||
|
||||
verify(validationManager).cancelCheck(MOCK_MODEL_1)
|
||||
verify(notificationManager).cancelStatusNotification(MOCK_MODEL_1)
|
|
@ -45,6 +45,7 @@ class AppDatabaseTest() {
|
|||
private lateinit var sitesDao: SiteDao
|
||||
private lateinit var settingsDao: SiteSettingsDao
|
||||
private lateinit var resultsDao: ValidationResultsDao
|
||||
private lateinit var retryDao: RetryPolicyDao
|
||||
|
||||
@Before fun setup() {
|
||||
val context = getApplicationContext<Context>()
|
||||
|
@ -52,6 +53,7 @@ class AppDatabaseTest() {
|
|||
sitesDao = db.siteDao()
|
||||
settingsDao = db.siteSettingsDao()
|
||||
resultsDao = db.validationResultsDao()
|
||||
retryDao = db.retryPolicyDao()
|
||||
}
|
||||
|
||||
@After
|
||||
|
@ -67,7 +69,8 @@ class AppDatabaseTest() {
|
|||
name = "Test 1",
|
||||
url = "https://test1.com",
|
||||
settings = null,
|
||||
lastResult = null
|
||||
lastResult = null,
|
||||
retryPolicy = null
|
||||
)
|
||||
val newId1 = sitesDao.insert(model1)
|
||||
assertThat(newId1).isGreaterThan(0)
|
||||
|
@ -76,7 +79,8 @@ class AppDatabaseTest() {
|
|||
name = "Test 2",
|
||||
url = "https://test2.com",
|
||||
settings = null,
|
||||
lastResult = null
|
||||
lastResult = null,
|
||||
retryPolicy = null
|
||||
)
|
||||
val newId2 = sitesDao.insert(model2)
|
||||
assertThat(newId2).isGreaterThan(newId1)
|
||||
|
@ -92,7 +96,8 @@ class AppDatabaseTest() {
|
|||
name = "Test",
|
||||
url = "https://test.com",
|
||||
settings = null,
|
||||
lastResult = null
|
||||
lastResult = null,
|
||||
retryPolicy = null
|
||||
)
|
||||
val newId = sitesDao.insert(model)
|
||||
assertThat(newId).isGreaterThan(0)
|
||||
|
@ -106,7 +111,8 @@ class AppDatabaseTest() {
|
|||
name = "Test 1",
|
||||
url = "https://test1.com",
|
||||
settings = null,
|
||||
lastResult = null
|
||||
lastResult = null,
|
||||
retryPolicy = null
|
||||
)
|
||||
val newId = sitesDao.insert(initialModel)
|
||||
assertThat(newId).isGreaterThan(0)
|
||||
|
@ -129,7 +135,8 @@ class AppDatabaseTest() {
|
|||
name = "Test 1",
|
||||
url = "https://test1.com",
|
||||
settings = null,
|
||||
lastResult = null
|
||||
lastResult = null,
|
||||
retryPolicy = null
|
||||
)
|
||||
val newId1 = sitesDao.insert(model1)
|
||||
assertThat(newId1).isGreaterThan(0)
|
||||
|
@ -138,7 +145,8 @@ class AppDatabaseTest() {
|
|||
name = "Test 2",
|
||||
url = "https://test2.com",
|
||||
settings = null,
|
||||
lastResult = null
|
||||
lastResult = null,
|
||||
retryPolicy = null
|
||||
)
|
||||
val newId2 = sitesDao.insert(model2)
|
||||
assertThat(newId2).isGreaterThan(newId1)
|
||||
|
@ -272,6 +280,59 @@ class AppDatabaseTest() {
|
|||
assertThat(resultsDao.forSite(1)).isEmpty()
|
||||
}
|
||||
|
||||
// RetryPolicyDao
|
||||
|
||||
@Test fun retryPolicy_insert_and_forSite() {
|
||||
val model = RetryPolicy(
|
||||
siteId = 1,
|
||||
count = 3,
|
||||
minutes = 6
|
||||
)
|
||||
val newId = retryDao.insert(model)
|
||||
assertThat(newId).isEqualTo(1)
|
||||
|
||||
val finalModel = resultsDao.forSite(newId)
|
||||
.single()
|
||||
assertThat(finalModel).isEqualTo(model.copy(siteId = newId))
|
||||
}
|
||||
|
||||
@Test fun retryPolicy_update() {
|
||||
retryDao.insert(
|
||||
RetryPolicy(
|
||||
siteId = 1,
|
||||
count = 3,
|
||||
minutes = 6
|
||||
)
|
||||
)
|
||||
|
||||
val insertedModel = retryDao.forSite(1)
|
||||
.single()
|
||||
val updatedModel = insertedModel.copy(
|
||||
count = 4,
|
||||
minutes = 8
|
||||
)
|
||||
assertThat(retryDao.update(updatedModel)).isEqualTo(1)
|
||||
|
||||
val finalModel = retryDao.forSite(1)
|
||||
.single()
|
||||
assertThat(finalModel).isEqualTo(updatedModel)
|
||||
}
|
||||
|
||||
@Test fun retryPolicy_delete() {
|
||||
retryDao.insert(
|
||||
RetryPolicy(
|
||||
siteId = 1,
|
||||
count = 3,
|
||||
minutes = 6
|
||||
)
|
||||
)
|
||||
|
||||
val insertedModel = retryDao.forSite(1)
|
||||
.single()
|
||||
retryDao.delete(insertedModel)
|
||||
assertThat(retryDao.forSite(1)).isEmpty()
|
||||
}
|
||||
|
||||
// Extension Methods
|
||||
|
||||
@Test fun extension_put_and_allSites() {
|
||||
|
@ -314,11 +375,16 @@ class AppDatabaseTest() {
|
|||
status = ERROR,
|
||||
reason = "Oh no"
|
||||
)
|
||||
val updatedRetryPolicy = modelToUpdate.retryPolicy!!.copy(
|
||||
count = 4,
|
||||
minutes = 8
|
||||
)
|
||||
val updatedModel = modelToUpdate.copy(
|
||||
name = "Oijrfouhef",
|
||||
url = "https://iojfdfsdk.io",
|
||||
settings = updatedSettings,
|
||||
lastResult = updatedValidationResult
|
||||
lastResult = updatedValidationResult,
|
||||
retryPolicy = updatedRetryPolicy
|
||||
)
|
||||
|
||||
db.updateSite(updatedModel)
|
||||
|
@ -326,6 +392,7 @@ class AppDatabaseTest() {
|
|||
val finalSite = db.getSite(modelToUpdate.id)!!
|
||||
assertThat(finalSite.settings).isEqualTo(updatedSettings)
|
||||
assertThat(finalSite.lastResult).isEqualTo(updatedValidationResult)
|
||||
assertThat(finalSite.retryPolicy).isEqualTo(updatedRetryPolicy)
|
||||
assertThat(finalSite).isEqualTo(updatedModel)
|
||||
}
|
||||
|
||||
|
@ -346,5 +413,10 @@ class AppDatabaseTest() {
|
|||
assertThat(remainingResults.size).isEqualTo(2)
|
||||
assertThat(remainingResults[0]).isEqualTo(allSites[0].lastResult!!)
|
||||
assertThat(remainingResults[1]).isEqualTo(allSites[2].lastResult!!)
|
||||
|
||||
val remainingRetryPolicies = retryDao.all()
|
||||
assertThat(remainingRetryPolicies.size).isEqualTo(2)
|
||||
assertThat(remainingRetryPolicies[0]).isEqualTo(allSites[0].retryPolicy!!)
|
||||
assertThat(remainingRetryPolicies[1]).isEqualTo(allSites[2].retryPolicy!!)
|
||||
}
|
||||
}
|
|
@ -47,12 +47,23 @@ fun fakeResultModel(
|
|||
timestampMs = currentTimeMillis()
|
||||
)
|
||||
|
||||
fun fakeRetryPolicy(
|
||||
id: Long,
|
||||
count: Int = 3,
|
||||
minutes: Int = 6
|
||||
) = RetryPolicy(
|
||||
siteId = id,
|
||||
count = count,
|
||||
minutes = minutes
|
||||
)
|
||||
|
||||
fun fakeModel(id: Long) = Site(
|
||||
id = id,
|
||||
name = "Test",
|
||||
url = "https://test.com",
|
||||
settings = fakeSettingsModel(id),
|
||||
lastResult = fakeResultModel(id)
|
||||
lastResult = fakeResultModel(id),
|
||||
retryPolicy = fakeRetryPolicy(id)
|
||||
)
|
||||
|
||||
val MOCK_MODEL_1 = fakeModel(1)
|
|
@ -26,11 +26,12 @@ import com.afollestad.nocknock.data.model.ValidationResult
|
|||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Database(
|
||||
entities = [
|
||||
RetryPolicy::class,
|
||||
ValidationResult::class,
|
||||
SiteSettings::class,
|
||||
Site::class
|
||||
],
|
||||
version = 1,
|
||||
version = 2,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
@ -41,6 +42,8 @@ abstract class AppDatabase : RoomDatabase() {
|
|||
abstract fun siteSettingsDao(): SiteSettingsDao
|
||||
|
||||
abstract fun validationResultsDao(): ValidationResultsDao
|
||||
|
||||
abstract fun retryPolicyDao(): RetryPolicyDao
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,9 +58,12 @@ fun AppDatabase.allSites(): List<Site> {
|
|||
.single()
|
||||
val lastResult = validationResultsDao().forSite(it.id)
|
||||
.singleOrNull()
|
||||
val retryPolicy = retryPolicyDao().forSite(it.id)
|
||||
.singleOrNull()
|
||||
return@map it.copy(
|
||||
settings = settings,
|
||||
lastResult = lastResult
|
||||
lastResult = lastResult,
|
||||
retryPolicy = retryPolicy
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -74,9 +80,12 @@ fun AppDatabase.getSite(id: Long): Site? {
|
|||
.single()
|
||||
val lastResult = validationResultsDao().forSite(id)
|
||||
.singleOrNull()
|
||||
val retryPolicy = retryPolicyDao().forSite(id)
|
||||
.singleOrNull()
|
||||
return result.copy(
|
||||
settings = settings,
|
||||
lastResult = lastResult
|
||||
lastResult = lastResult,
|
||||
retryPolicy = retryPolicy
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -86,14 +95,14 @@ fun AppDatabase.getSite(id: Long): Site? {
|
|||
* @author Aidan Follestad (@afollestad)
|
||||
*/
|
||||
fun AppDatabase.putSite(site: Site): Site {
|
||||
requireNotNull(site.settings) { "Settings must be populated." }
|
||||
val settings = site.settings ?: throw IllegalArgumentException("Settings cannot be null.")
|
||||
val newId = siteDao().insert(site)
|
||||
val settingsWithSiteId =
|
||||
site.settings!!.copy(
|
||||
siteId = newId
|
||||
)
|
||||
val settingsWithSiteId = settings.copy(siteId = newId)
|
||||
siteSettingsDao().insert(settingsWithSiteId)
|
||||
|
||||
site.lastResult?.let { validationResultsDao().insert(it) }
|
||||
site.retryPolicy?.let { retryPolicyDao().insert(it) }
|
||||
|
||||
return site.copy(
|
||||
id = newId,
|
||||
settings = settingsWithSiteId
|
||||
|
@ -107,26 +116,37 @@ fun AppDatabase.putSite(site: Site): Site {
|
|||
*/
|
||||
fun AppDatabase.updateSite(site: Site) {
|
||||
siteDao().update(site)
|
||||
if (site.settings != null) {
|
||||
|
||||
val settings = site.settings?.copy(siteId = site.id)
|
||||
if (settings != null) {
|
||||
val existing = siteSettingsDao().forSite(site.id)
|
||||
.singleOrNull()
|
||||
if (existing != null) {
|
||||
siteSettingsDao().update(site.settings!!)
|
||||
siteSettingsDao().update(settings)
|
||||
} else {
|
||||
siteSettingsDao().insert(
|
||||
site.settings!!.copy(
|
||||
siteId = site.id
|
||||
)
|
||||
)
|
||||
siteSettingsDao().insert(settings)
|
||||
}
|
||||
}
|
||||
if (site.lastResult != null) {
|
||||
|
||||
val lastResult = site.lastResult?.copy(siteId = site.id)
|
||||
if (lastResult != null) {
|
||||
val existing = validationResultsDao().forSite(site.id)
|
||||
.singleOrNull()
|
||||
if (existing != null) {
|
||||
validationResultsDao().update(site.lastResult!!)
|
||||
validationResultsDao().update(lastResult)
|
||||
} else {
|
||||
validationResultsDao().insert(site.lastResult!!)
|
||||
validationResultsDao().insert(lastResult)
|
||||
}
|
||||
}
|
||||
|
||||
val retryPolicy = site.retryPolicy?.copy(siteId = site.id)
|
||||
if (retryPolicy != null) {
|
||||
val existing = retryPolicyDao().forSite(site.id)
|
||||
.singleOrNull()
|
||||
if (existing != null) {
|
||||
retryPolicyDao().update(retryPolicy)
|
||||
} else {
|
||||
retryPolicyDao().insert(retryPolicy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -137,11 +157,8 @@ fun AppDatabase.updateSite(site: Site) {
|
|||
* @author Aidan Follestad (@afollestad)
|
||||
*/
|
||||
fun AppDatabase.deleteSite(site: Site) {
|
||||
if (site.settings != null) {
|
||||
siteSettingsDao().delete(site.settings!!)
|
||||
}
|
||||
if (site.lastResult != null) {
|
||||
validationResultsDao().delete(site.lastResult!!)
|
||||
}
|
||||
site.settings?.let { siteSettingsDao().delete(it) }
|
||||
site.lastResult?.let { validationResultsDao().delete(it) }
|
||||
site.retryPolicy?.let { retryPolicyDao().delete(it) }
|
||||
siteDao().delete(site)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* 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.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
/**
|
||||
* Migrates the database from version 1 to 2.
|
||||
*
|
||||
* @author Aidan Follestad (@afollestad)
|
||||
*/
|
||||
class Database1to2Migration : Migration(1, 2) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"CREATE TABLE `retry_policies` (siteId INTEGER PRIMARY KEY NOT NULL, count INTEGER NOT NULL, minutes INTEGER NOT NULL)"
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* 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
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.afollestad.nocknock.utilities.ext.MINUTE
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Represents a site's retry policy, or how many times we
|
||||
* retry in a certain timespan before considering a site to
|
||||
* have a problem.
|
||||
*
|
||||
* @author Aidan Follestad (@afollestad)
|
||||
*/
|
||||
@Entity(tableName = "retry_policies")
|
||||
data class RetryPolicy(
|
||||
/** The [Site] these settings belong to. */
|
||||
@PrimaryKey(autoGenerate = false) var siteId: Long = 0,
|
||||
/** How many times we want to retry. */
|
||||
var count: Int = 0,
|
||||
/**
|
||||
* In what amount of time (in minutes) we want
|
||||
* to perform those retries.
|
||||
*/
|
||||
var minutes: Int = 0
|
||||
) : Serializable {
|
||||
|
||||
constructor() : this(0, 0, 0)
|
||||
|
||||
// Say we are trying 6 times in 3 minutes, that means times per minute = 2.
|
||||
// Twice per minute means every 30 seconds.
|
||||
// 30 seconds = 30 * 1000 or 30,000 milliseconds.
|
||||
// 60,000 / 2 = 30,000.
|
||||
fun interval(): Long {
|
||||
val timesPerMinute = count.toFloat() / minutes.toFloat()
|
||||
return MINUTE / timesPerMinute.toInt()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* 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
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Dao
|
||||
interface RetryPolicyDao {
|
||||
|
||||
@Query("SELECT * FROM retry_policies ORDER BY siteId ASC")
|
||||
fun all(): List<RetryPolicy>
|
||||
|
||||
@Query("SELECT * FROM retry_policies WHERE siteId = :siteId LIMIT 1")
|
||||
fun forSite(siteId: Long): List<RetryPolicy>
|
||||
|
||||
@Insert(onConflict = FAIL)
|
||||
fun insert(policy: RetryPolicy): Long
|
||||
|
||||
@Update(onConflict = FAIL)
|
||||
fun update(policy: RetryPolicy): Int
|
||||
|
||||
@Delete
|
||||
fun delete(policy: RetryPolicy): Int
|
||||
}
|
|
@ -18,6 +18,7 @@ package com.afollestad.nocknock.data.model
|
|||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
import com.afollestad.nocknock.data.RetryPolicy
|
||||
import com.afollestad.nocknock.data.model.Status.WAITING
|
||||
import com.afollestad.nocknock.utilities.ext.timeString
|
||||
import com.afollestad.nocknock.utilities.providers.CanNotifyModel
|
||||
|
@ -36,10 +37,12 @@ data class Site(
|
|||
/** Settings for the site. */
|
||||
@Ignore var settings: SiteSettings?,
|
||||
/** 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. */
|
||||
@Ignore var retryPolicy: RetryPolicy?
|
||||
) : CanNotifyModel {
|
||||
|
||||
constructor() : this(0, "", "", null, null)
|
||||
constructor() : this(0, "", "", null, null, null)
|
||||
|
||||
override fun notifyId(): Int = id.toInt()
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* 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.retrypolicy
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.LinearLayout
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.afollestad.nocknock.viewcomponents.R
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData
|
||||
import com.afollestad.nocknock.viewcomponents.livedata.lifecycleOwner
|
||||
import kotlinx.android.synthetic.main.retry_policy_layout.view.minutes
|
||||
import kotlinx.android.synthetic.main.retry_policy_layout.view.times
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class RetryPolicyLayout(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : LinearLayout(context, attrs) {
|
||||
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
inflate(context, R.layout.retry_policy_layout, this)
|
||||
}
|
||||
|
||||
fun attach(
|
||||
timesData: MutableLiveData<Int>,
|
||||
minutesData: MutableLiveData<Int>
|
||||
) {
|
||||
times.attachLiveData(lifecycleOwner(), timesData)
|
||||
minutes.attachLiveData(lifecycleOwner(), minutesData)
|
||||
}
|
||||
}
|
82
viewcomponents/src/main/res/layout/retry_policy_layout.xml
Normal file
82
viewcomponents/src/main/res/layout/retry_policy_layout.xml
Normal file
|
@ -0,0 +1,82 @@
|
|||
<?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"
|
||||
>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/retry_policy"
|
||||
style="@style/NockText.SectionHeader"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/content_inset_quarter"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/content_inset"
|
||||
android:text="@string/retry_policy_retry"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/times"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="0"
|
||||
android:inputType="number"
|
||||
android:maxLength="4"
|
||||
android:nextFocusDown="@+id/minutes"
|
||||
android:nextFocusRight="@+id/minutes"
|
||||
tools:ignore="Autofill,HardcodedText,LabelFor"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/content_inset"
|
||||
android:layout_marginStart="@dimen/content_inset"
|
||||
android:text="@string/retry_policy_times_in"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/minutes"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="0"
|
||||
android:inputType="number"
|
||||
android:maxLength="4"
|
||||
android:nextFocusLeft="@+id/times"
|
||||
android:nextFocusUp="@+id/times"
|
||||
tools:ignore="Autofill,HardcodedText,LabelFor"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/content_inset"
|
||||
android:text="@string/retry_policy_minutes"
|
||||
style="@style/NockText.Body"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</merge>
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<dimen name="default_elevation">4dp</dimen>
|
||||
|
||||
<dimen name="content_inset_half_quarter">4dp</dimen>
|
||||
<dimen name="content_inset_quarter">4dp</dimen>
|
||||
<dimen name="content_inset_half">8dp</dimen>
|
||||
<dimen name="content_inset_less">12dp</dimen>
|
||||
<dimen name="content_inset">16dp</dimen>
|
||||
|
|
|
@ -7,4 +7,9 @@
|
|||
|
||||
<string name="check_interval">Check Interval</string>
|
||||
|
||||
<string name="retry_policy">Retry Policy</string>
|
||||
<string name="retry_policy_retry">Retry</string>
|
||||
<string name="retry_policy_times_in">times in</string>
|
||||
<string name="retry_policy_minutes">Minutes</string>
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Add table
Reference in a new issue