diff --git a/app/src/main/java/com/afollestad/nocknock/AppExt.kt b/app/src/main/kotlin/com/afollestad/nocknock/AppExt.kt similarity index 100% rename from app/src/main/java/com/afollestad/nocknock/AppExt.kt rename to app/src/main/kotlin/com/afollestad/nocknock/AppExt.kt diff --git a/app/src/main/java/com/afollestad/nocknock/NockNockApp.kt b/app/src/main/kotlin/com/afollestad/nocknock/NockNockApp.kt similarity index 100% rename from app/src/main/java/com/afollestad/nocknock/NockNockApp.kt rename to app/src/main/kotlin/com/afollestad/nocknock/NockNockApp.kt diff --git a/app/src/main/java/com/afollestad/nocknock/adapter/SiteAdapter.kt b/app/src/main/kotlin/com/afollestad/nocknock/adapter/SiteAdapter.kt similarity index 100% rename from app/src/main/java/com/afollestad/nocknock/adapter/SiteAdapter.kt rename to app/src/main/kotlin/com/afollestad/nocknock/adapter/SiteAdapter.kt diff --git a/app/src/main/java/com/afollestad/nocknock/adapter/SiteDiffCallback.kt b/app/src/main/kotlin/com/afollestad/nocknock/adapter/SiteDiffCallback.kt similarity index 100% rename from app/src/main/java/com/afollestad/nocknock/adapter/SiteDiffCallback.kt rename to app/src/main/kotlin/com/afollestad/nocknock/adapter/SiteDiffCallback.kt diff --git a/app/src/main/java/com/afollestad/nocknock/broadcasts/StatusUpdateIntentReceiver.kt b/app/src/main/kotlin/com/afollestad/nocknock/broadcasts/StatusUpdateIntentReceiver.kt similarity index 100% rename from app/src/main/java/com/afollestad/nocknock/broadcasts/StatusUpdateIntentReceiver.kt rename to app/src/main/kotlin/com/afollestad/nocknock/broadcasts/StatusUpdateIntentReceiver.kt diff --git a/app/src/main/java/com/afollestad/nocknock/dialogs/AboutDialog.kt b/app/src/main/kotlin/com/afollestad/nocknock/dialogs/AboutDialog.kt similarity index 100% rename from app/src/main/java/com/afollestad/nocknock/dialogs/AboutDialog.kt rename to app/src/main/kotlin/com/afollestad/nocknock/dialogs/AboutDialog.kt diff --git a/app/src/main/java/com/afollestad/nocknock/koin/MainModule.kt b/app/src/main/kotlin/com/afollestad/nocknock/koin/MainModule.kt similarity index 100% rename from app/src/main/java/com/afollestad/nocknock/koin/MainModule.kt rename to app/src/main/kotlin/com/afollestad/nocknock/koin/MainModule.kt diff --git a/app/src/main/java/com/afollestad/nocknock/koin/ViewModelModule.kt b/app/src/main/kotlin/com/afollestad/nocknock/koin/ViewModelModule.kt similarity index 100% rename from app/src/main/java/com/afollestad/nocknock/koin/ViewModelModule.kt rename to app/src/main/kotlin/com/afollestad/nocknock/koin/ViewModelModule.kt diff --git a/app/src/main/java/com/afollestad/nocknock/ui/ScopedViewModel.kt b/app/src/main/kotlin/com/afollestad/nocknock/ui/ScopedViewModel.kt similarity index 100% rename from app/src/main/java/com/afollestad/nocknock/ui/ScopedViewModel.kt rename to app/src/main/kotlin/com/afollestad/nocknock/ui/ScopedViewModel.kt diff --git a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt b/app/src/main/kotlin/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt similarity index 89% rename from app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt rename to app/src/main/kotlin/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt index d0b9376..9e91397 100644 --- a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt +++ b/app/src/main/kotlin/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt @@ -21,12 +21,12 @@ import android.widget.ArrayAdapter import androidx.appcompat.app.AppCompatActivity import com.afollestad.nocknock.R import com.afollestad.nocknock.data.model.ValidationMode -import com.afollestad.nocknock.viewcomponents.ext.attachLiveData import com.afollestad.nocknock.viewcomponents.ext.conceal import com.afollestad.nocknock.viewcomponents.ext.onLayout -import com.afollestad.nocknock.viewcomponents.ext.toViewError -import com.afollestad.nocknock.viewcomponents.ext.toViewText -import com.afollestad.nocknock.viewcomponents.ext.toViewVisibility +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.inputName @@ -88,9 +88,11 @@ class AddSiteActivity : AppCompatActivity() { .toViewError(this, responseTimeoutInput) // Validation mode - responseValidationMode.attachLiveData(this, viewModel.validationMode, - { ValidationMode.fromIndex(it) }, - { it.toIndex() } + responseValidationMode.attachLiveData( + lifecycleOwner = this, + data = viewModel.validationMode, + outTransformer = { ValidationMode.fromIndex(it) }, + inTransformer = { it.toIndex() } ) viewModel.onValidationSearchTermError() .toViewError(this, responseValidationSearchTerm) @@ -98,7 +100,11 @@ class AddSiteActivity : AppCompatActivity() { .toViewText(this, validationModeDescription) // Validation search term - responseValidationSearchTerm.attachLiveData(this, viewModel.validationSearchTerm) + responseValidationSearchTerm.attachLiveData( + lifecycleOwner = this, + data = viewModel.validationSearchTerm, + pullInChanges = false + ) viewModel.onValidationSearchTermVisibility() .toViewVisibility(this, responseValidationSearchTerm) diff --git a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivityExt.kt b/app/src/main/kotlin/com/afollestad/nocknock/ui/addsite/AddSiteActivityExt.kt similarity index 100% rename from app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivityExt.kt rename to app/src/main/kotlin/com/afollestad/nocknock/ui/addsite/AddSiteActivityExt.kt diff --git a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteViewModel.kt b/app/src/main/kotlin/com/afollestad/nocknock/ui/addsite/AddSiteViewModel.kt similarity index 97% rename from app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteViewModel.kt rename to app/src/main/kotlin/com/afollestad/nocknock/ui/addsite/AddSiteViewModel.kt index 4f51b73..9b388b1 100644 --- a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteViewModel.kt +++ b/app/src/main/kotlin/com/afollestad/nocknock/ui/addsite/AddSiteViewModel.kt @@ -32,8 +32,8 @@ import com.afollestad.nocknock.data.model.ValidationMode.TERM_SEARCH import com.afollestad.nocknock.data.putSite import com.afollestad.nocknock.engine.validation.ValidationManager import com.afollestad.nocknock.ui.ScopedViewModel +import com.afollestad.nocknock.utilities.livedata.map import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan -import com.afollestad.nocknock.viewcomponents.ext.map import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -50,7 +50,9 @@ class AddSiteViewModel( // Public properties val name = MutableLiveData() val url = MutableLiveData() - val timeout = MutableLiveData() + val timeout = MutableLiveData().apply { + this.value = 10000 + } val validationMode = MutableLiveData() val validationSearchTerm = MutableLiveData() val validationScript = MutableLiveData() @@ -214,8 +216,8 @@ class AddSiteViewModel( ) return Site( id = 0, - name = name.value!!, - url = url.value!!, + name = name.value!!.trim(), + url = url.value!!.trim(), settings = newSettings, lastResult = null ) diff --git a/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivity.kt b/app/src/main/kotlin/com/afollestad/nocknock/ui/main/MainActivity.kt similarity index 100% rename from app/src/main/java/com/afollestad/nocknock/ui/main/MainActivity.kt rename to app/src/main/kotlin/com/afollestad/nocknock/ui/main/MainActivity.kt diff --git a/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivityExt.kt b/app/src/main/kotlin/com/afollestad/nocknock/ui/main/MainActivityExt.kt similarity index 100% rename from app/src/main/java/com/afollestad/nocknock/ui/main/MainActivityExt.kt rename to app/src/main/kotlin/com/afollestad/nocknock/ui/main/MainActivityExt.kt diff --git a/app/src/main/java/com/afollestad/nocknock/ui/main/MainViewModel.kt b/app/src/main/kotlin/com/afollestad/nocknock/ui/main/MainViewModel.kt similarity index 100% rename from app/src/main/java/com/afollestad/nocknock/ui/main/MainViewModel.kt rename to app/src/main/kotlin/com/afollestad/nocknock/ui/main/MainViewModel.kt diff --git a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt b/app/src/main/kotlin/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt similarity index 93% rename from app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt rename to app/src/main/kotlin/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt index fddf3d7..76011bc 100644 --- a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt +++ b/app/src/main/kotlin/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt @@ -26,12 +26,12 @@ import com.afollestad.nocknock.broadcasts.StatusUpdateIntentReceiver import com.afollestad.nocknock.data.model.Site import com.afollestad.nocknock.data.model.ValidationMode import com.afollestad.nocknock.utilities.providers.IntentProvider -import com.afollestad.nocknock.viewcomponents.ext.attachLiveData +import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData import com.afollestad.nocknock.viewcomponents.ext.dimenFloat import com.afollestad.nocknock.viewcomponents.ext.onScroll -import com.afollestad.nocknock.viewcomponents.ext.toViewError -import com.afollestad.nocknock.viewcomponents.ext.toViewText -import com.afollestad.nocknock.viewcomponents.ext.toViewVisibility +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 @@ -102,9 +102,12 @@ class ViewSiteActivity : AppCompatActivity() { .toViewError(this, responseTimeoutInput) // Validation mode - responseValidationMode.attachLiveData(this, viewModel.validationMode, - { ValidationMode.fromIndex(it) }, - { it.toIndex() }) + responseValidationMode.attachLiveData( + lifecycleOwner = this, + data = viewModel.validationMode, + outTransformer = { ValidationMode.fromIndex(it) }, + inTransformer = { it.toIndex() } + ) viewModel.onValidationSearchTermError() .toViewError(this, responseValidationSearchTerm) viewModel.onValidationModeDescription() diff --git a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivityExt.kt b/app/src/main/kotlin/com/afollestad/nocknock/ui/viewsite/ViewSiteActivityExt.kt similarity index 100% rename from app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivityExt.kt rename to app/src/main/kotlin/com/afollestad/nocknock/ui/viewsite/ViewSiteActivityExt.kt diff --git a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModel.kt b/app/src/main/kotlin/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModel.kt similarity index 97% rename from app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModel.kt rename to app/src/main/kotlin/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModel.kt index a88013c..57bc172 100644 --- a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModel.kt +++ b/app/src/main/kotlin/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModel.kt @@ -38,10 +38,10 @@ import com.afollestad.nocknock.engine.validation.ValidationManager import com.afollestad.nocknock.notifications.NockNotificationManager import com.afollestad.nocknock.ui.ScopedViewModel import com.afollestad.nocknock.utilities.ext.formatDate +import com.afollestad.nocknock.utilities.livedata.map +import com.afollestad.nocknock.utilities.livedata.zip import com.afollestad.nocknock.utilities.providers.StringProvider import com.afollestad.nocknock.viewcomponents.ext.isNullOrLessThan -import com.afollestad.nocknock.viewcomponents.ext.map -import com.afollestad.nocknock.viewcomponents.ext.zip import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -232,8 +232,8 @@ class ViewSiteViewModel( @VisibleForTesting(otherwise = PRIVATE) fun getValidationArgs(): String? { return when (validationMode.value) { - TERM_SEARCH -> validationSearchTerm.value - JAVASCRIPT -> validationScript.value + TERM_SEARCH -> validationSearchTerm.value?.trim() + JAVASCRIPT -> validationScript.value?.trim() else -> null } } @@ -310,8 +310,8 @@ class ViewSiteViewModel( disabled = false ) return site.copy( - name = name.value!!, - url = url.value!!, + name = name.value!!.trim(), + url = url.value!!.trim(), settings = newSettings ) .withStatus(status = WAITING) diff --git a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelExt.kt b/app/src/main/kotlin/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelExt.kt similarity index 100% rename from app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelExt.kt rename to app/src/main/kotlin/com/afollestad/nocknock/ui/viewsite/ViewSiteViewModelExt.kt diff --git a/app/src/main/res/layout/activity_addsite.xml b/app/src/main/res/layout/activity_addsite.xml index 69a29d9..8567019 100644 --- a/app/src/main/res/layout/activity_addsite.xml +++ b/app/src/main/res/layout/activity_addsite.xml @@ -93,7 +93,7 @@ - - Unit) { +fun EditText.setTextAndMaintainSelection(text: CharSequence) { + val formerStart = min(selectionStart, text.length) + val formerEnd = min(selectionEnd, text.length) + setText(text) + if (formerEnd <= formerStart) { + setSelection(formerStart) + } else { + setSelection(formerStart, formerEnd) + } +} + +fun EditText.onTextChanged( + @IntRange(from = 0, to = 10000) debounce: Int = 0, + cb: (String) -> Unit +) { addTextChangedListener(object : TextWatcher { + val callbackRunner = Runnable { + cb(text.trim().toString()) + } + override fun afterTextChanged(s: Editable?) = Unit override fun beforeTextChanged( @@ -35,6 +55,13 @@ fun EditText.onTextChanged(cb: (String) -> Unit) { start: Int, before: Int, count: Int - ) = cb(s.toString().trim()) + ) { + removeCallbacks(callbackRunner) + if (debounce == 0) { + callbackRunner.run() + } else { + postDelayed(callbackRunner, debounce.toLong()) + } + } }) } diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/js/JavaScript.kt b/common/src/main/kotlin/com/afollestad/nocknock/utilities/js/JavaScript.kt similarity index 100% rename from common/src/main/java/com/afollestad/nocknock/utilities/js/JavaScript.kt rename to common/src/main/kotlin/com/afollestad/nocknock/utilities/js/JavaScript.kt diff --git a/common/src/main/kotlin/com/afollestad/nocknock/utilities/livedata/Distinct.kt b/common/src/main/kotlin/com/afollestad/nocknock/utilities/livedata/Distinct.kt new file mode 100644 index 0000000..d6a26d2 --- /dev/null +++ b/common/src/main/kotlin/com/afollestad/nocknock/utilities/livedata/Distinct.kt @@ -0,0 +1,53 @@ +/** + * 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.utilities.livedata + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.Observer + +/** @author Aidan Follestad (@afollestad) */ +class DistinctLiveData(source1: LiveData) : MediatorLiveData() { + + private var isInitialized = false + private var lastValue: T? = null + + init { + super.addSource(source1) { + if (!isInitialized) { + value = it + isInitialized = true + lastValue = it + } else if (lastValue != it) { + value = it + lastValue = it + } + } + } + + override fun addSource( + source: LiveData, + onChanged: Observer + ) { + throw UnsupportedOperationException() + } + + override fun removeSource(toRemote: LiveData) { + throw UnsupportedOperationException() + } +} + +fun LiveData.distinct(): MediatorLiveData = DistinctLiveData(this) diff --git a/common/src/main/kotlin/com/afollestad/nocknock/utilities/livedata/LiveDataExt.kt b/common/src/main/kotlin/com/afollestad/nocknock/utilities/livedata/LiveDataExt.kt new file mode 100644 index 0000000..cacdc92 --- /dev/null +++ b/common/src/main/kotlin/com/afollestad/nocknock/utilities/livedata/LiveDataExt.kt @@ -0,0 +1,25 @@ +/** + * 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.utilities.livedata + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations + +fun LiveData.map(mapper: (X) -> Y) = + Transformations.map(this, mapper)!! + +fun LiveData.switchMap(mapper: (X) -> LiveData) = + Transformations.switchMap(this, mapper)!! diff --git a/app/src/test/java/com/afollestad/nocknock/LiveDataTestUtil.kt b/common/src/main/kotlin/com/afollestad/nocknock/utilities/livedata/TestLiveData.kt similarity index 69% rename from app/src/test/java/com/afollestad/nocknock/LiveDataTestUtil.kt rename to common/src/main/kotlin/com/afollestad/nocknock/utilities/livedata/TestLiveData.kt index 0556668..7b94e95 100644 --- a/app/src/test/java/com/afollestad/nocknock/LiveDataTestUtil.kt +++ b/common/src/main/kotlin/com/afollestad/nocknock/utilities/livedata/TestLiveData.kt @@ -13,13 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.afollestad.nocknock +package com.afollestad.nocknock.utilities.livedata import androidx.annotation.CheckResult import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import com.google.common.truth.Truth.assertThat -import com.google.common.truth.Truth.assertWithMessage /** @author Aidan Follestad (@afollestad) */ class TestLiveData(data: LiveData) { @@ -34,17 +32,32 @@ class TestLiveData(data: LiveData) { } fun assertNoValues() { - assertWithMessage("Expected no values, but got: $receivedValues").that(receivedValues) - .isEmpty() + if (receivedValues.isNotEmpty()) { + throw AssertionError("Expected no values, but got: $receivedValues") + } } fun assertValues(vararg assertValues: T) { val assertList = assertValues.toList() - assertThat(receivedValues).isEqualTo(assertList) + if (!assertList.contentEquals(receivedValues)) { + throw AssertionError("Expected $assertList\n\t\tBut got: $receivedValues") + } receivedValues.clear() } @CheckResult fun values(): List = receivedValues + + private fun List.contentEquals(other: List): Boolean { + if (this.size != other.size) { + return false + } + for ((index, value) in this.withIndex()) { + if (other[index] != value) { + return false + } + } + return true + } } @CheckResult fun LiveData.test() = TestLiveData(this) diff --git a/common/src/main/kotlin/com/afollestad/nocknock/utilities/livedata/Zip.kt b/common/src/main/kotlin/com/afollestad/nocknock/utilities/livedata/Zip.kt new file mode 100644 index 0000000..739fa5b --- /dev/null +++ b/common/src/main/kotlin/com/afollestad/nocknock/utilities/livedata/Zip.kt @@ -0,0 +1,102 @@ +/** + * 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.utilities.livedata + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.Observer + +typealias Zipper = (T, K) -> R + +/** @author Aidan Follestad (@afollestad) */ +class ZipLiveData( + source1: LiveData, + source2: LiveData, + private val distinctUntilChanged: Boolean, + private val resetAfterEmission: Boolean, + private val zipper: Zipper +) : MediatorLiveData() { + + private var data1: T? = null + private var data2: K? = null + private var lastNotified: R? = null + + init { + super.addSource(source1) { + if (data1 == it) return@addSource + data1 = it + maybeNotify() + } + super.addSource(source2) { + if (data2 == it) return@addSource + data2 = it + maybeNotify() + } + } + + private fun maybeNotify() { + if (data1 != null && data2 != null) { + val zippedUp = zipper(data1!!, data2!!) + + if (!distinctUntilChanged || zippedUp != lastNotified) { + value = zippedUp + lastNotified = zippedUp + + if (resetAfterEmission) { + data1 = null + data2 = null + } + } + } + } + + override fun addSource( + source: LiveData, + onChanged: Observer + ) { + throw UnsupportedOperationException() + } + + override fun removeSource(toRemote: LiveData) { + throw UnsupportedOperationException() + } +} + +fun zip( + source1: LiveData, + source2: LiveData, + distinctUntilChanged: Boolean = true, + resetAfterEmission: Boolean = false, + zipper: Zipper +) = ZipLiveData( + source1 = source1, + source2 = source2, + distinctUntilChanged = distinctUntilChanged, + resetAfterEmission = resetAfterEmission, + zipper = zipper +) + +fun zip( + source1: LiveData, + source2: LiveData, + distinctUntilChanged: Boolean = true, + resetAfterEmission: Boolean = false +) = zip( + source1 = source1, + source2 = source2, + distinctUntilChanged = distinctUntilChanged, + resetAfterEmission = resetAfterEmission, + zipper = { left, right -> Pair(left, right) }) diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/providers/BundleProvider.kt b/common/src/main/kotlin/com/afollestad/nocknock/utilities/providers/BundleProvider.kt similarity index 100% rename from common/src/main/java/com/afollestad/nocknock/utilities/providers/BundleProvider.kt rename to common/src/main/kotlin/com/afollestad/nocknock/utilities/providers/BundleProvider.kt diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/providers/IntentProvider.kt b/common/src/main/kotlin/com/afollestad/nocknock/utilities/providers/IntentProvider.kt similarity index 100% rename from common/src/main/java/com/afollestad/nocknock/utilities/providers/IntentProvider.kt rename to common/src/main/kotlin/com/afollestad/nocknock/utilities/providers/IntentProvider.kt diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/providers/JobInfoProvider.kt b/common/src/main/kotlin/com/afollestad/nocknock/utilities/providers/JobInfoProvider.kt similarity index 100% rename from common/src/main/java/com/afollestad/nocknock/utilities/providers/JobInfoProvider.kt rename to common/src/main/kotlin/com/afollestad/nocknock/utilities/providers/JobInfoProvider.kt diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/providers/NotificationChannelProvider.kt b/common/src/main/kotlin/com/afollestad/nocknock/utilities/providers/NotificationChannelProvider.kt similarity index 100% rename from common/src/main/java/com/afollestad/nocknock/utilities/providers/NotificationChannelProvider.kt rename to common/src/main/kotlin/com/afollestad/nocknock/utilities/providers/NotificationChannelProvider.kt diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/providers/NotificationProvider.kt b/common/src/main/kotlin/com/afollestad/nocknock/utilities/providers/NotificationProvider.kt similarity index 100% rename from common/src/main/java/com/afollestad/nocknock/utilities/providers/NotificationProvider.kt rename to common/src/main/kotlin/com/afollestad/nocknock/utilities/providers/NotificationProvider.kt diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/providers/SdkProvider.kt b/common/src/main/kotlin/com/afollestad/nocknock/utilities/providers/SdkProvider.kt similarity index 100% rename from common/src/main/java/com/afollestad/nocknock/utilities/providers/SdkProvider.kt rename to common/src/main/kotlin/com/afollestad/nocknock/utilities/providers/SdkProvider.kt diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/providers/StringProvider.kt b/common/src/main/kotlin/com/afollestad/nocknock/utilities/providers/StringProvider.kt similarity index 100% rename from common/src/main/java/com/afollestad/nocknock/utilities/providers/StringProvider.kt rename to common/src/main/kotlin/com/afollestad/nocknock/utilities/providers/StringProvider.kt diff --git a/common/src/main/java/com/afollestad/nocknock/utilities/ui/DebouncedOnClickListener.kt b/common/src/main/kotlin/com/afollestad/nocknock/utilities/ui/DebouncedOnClickListener.kt similarity index 100% rename from common/src/main/java/com/afollestad/nocknock/utilities/ui/DebouncedOnClickListener.kt rename to common/src/main/kotlin/com/afollestad/nocknock/utilities/ui/DebouncedOnClickListener.kt diff --git a/common/src/test/kotlin/com/afollestad/nocknock/utilities/livedata/DistinctTest.kt b/common/src/test/kotlin/com/afollestad/nocknock/utilities/livedata/DistinctTest.kt new file mode 100644 index 0000000..465a085 --- /dev/null +++ b/common/src/test/kotlin/com/afollestad/nocknock/utilities/livedata/DistinctTest.kt @@ -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.utilities.livedata + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.MutableLiveData +import org.junit.Rule +import org.junit.Test + +/** @author Aidan Follestad (@afollestad) */ +class DistinctTest { + + @Rule @JvmField val rule = InstantTaskExecutorRule() + + @Test fun filterLastValues() { + val data = MutableLiveData() + val distinct = data.distinct() + .test() + + data.postValue("Hello") + data.postValue("Hello") + + data.postValue("Hi") + data.postValue("Hi") + + data.postValue("Hello") + + distinct.assertValues( + "Hello", + "Hi", + "Hello" + ) + } +} diff --git a/common/src/test/kotlin/com/afollestad/nocknock/utilities/livedata/ZipTest.kt b/common/src/test/kotlin/com/afollestad/nocknock/utilities/livedata/ZipTest.kt new file mode 100644 index 0000000..0cc57da --- /dev/null +++ b/common/src/test/kotlin/com/afollestad/nocknock/utilities/livedata/ZipTest.kt @@ -0,0 +1,123 @@ +/** + * 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.utilities.livedata + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.MutableLiveData +import org.junit.Rule +import org.junit.Test + +/** @author Aidan Follestad (@afollestad) */ +class ZipTest { + + @Rule @JvmField val rule = InstantTaskExecutorRule() + + @Test fun test_withDistinct() { + val data1 = MutableLiveData() + val data2 = MutableLiveData() + val zipped = zip(data1, data2, true) + .test() + + data1.postValue("Hello") + data2.postValue(24) + zipped.assertValues(Pair("Hello", 24)) + + data1.postValue("Hello") + data2.postValue(24) + zipped.assertNoValues() + } + + @Test fun test_noDistinct() { + val data1 = MutableLiveData() + val data2 = MutableLiveData() + val zipped = zip(data1, data2, false) + .test() + + data1.postValue("Hello") + data2.postValue(24) + zipped.assertValues(Pair("Hello", 24)) + + data1.postValue("Hi") + data2.postValue(24) + zipped.assertValues(Pair("Hi", 24)) + } + + @Test fun test_noDistinct_resetAfterEmission() { + val data1 = MutableLiveData() + val data2 = MutableLiveData() + val zipped = zip(data1, data2, false, true) + .test() + + data1.postValue("Hello") + data2.postValue(24) + zipped.assertValues(Pair("Hello", 24)) + + data1.postValue("Hi") + data2.postValue(50) + zipped.assertValues(Pair("Hi", 50)) + } + + @Test fun test_withDistinct_customZipper() { + val data1 = MutableLiveData() + val data2 = MutableLiveData() + val zipped = zip(data1, data2, true, + zipper = { left, right -> + "$left $right" + }).test() + + data1.postValue("Hello") + data2.postValue(24) + zipped.assertValues("Hello 24") + + data1.postValue("Hello") + data2.postValue(24) + zipped.assertNoValues() + } + + @Test fun test_noDistinct_customZipper() { + val data1 = MutableLiveData() + val data2 = MutableLiveData() + val zipped = zip(data1, data2, false, + zipper = { left, right -> + "$left $right" + }).test() + + data1.postValue("Hello") + data2.postValue(24) + zipped.assertValues("Hello 24") + + data1.postValue("Hi") + data2.postValue(24) + zipped.assertValues("Hi 24") + } + + @Test fun test_noDistinct_customZipper_resetAfterEmission() { + val data1 = MutableLiveData() + val data2 = MutableLiveData() + val zipped = zip(data1, data2, false, true, + zipper = { left, right -> + "$left $right" + }).test() + + data1.postValue("Hello") + data2.postValue(24) + zipped.assertValues("Hello 24") + + data1.postValue("Hi") + data2.postValue(50) + zipped.assertValues("Hi 50") + } +} diff --git a/data/src/androidTest/java/com/afollestad/nocknock/data/AppDatabaseTest.kt b/data/src/androidTest/kotlin/com/afollestad/nocknock/data/AppDatabaseTest.kt similarity index 100% rename from data/src/androidTest/java/com/afollestad/nocknock/data/AppDatabaseTest.kt rename to data/src/androidTest/kotlin/com/afollestad/nocknock/data/AppDatabaseTest.kt diff --git a/data/src/androidTest/java/com/afollestad/nocknock/data/TestUtil.kt b/data/src/androidTest/kotlin/com/afollestad/nocknock/data/TestUtil.kt similarity index 100% rename from data/src/androidTest/java/com/afollestad/nocknock/data/TestUtil.kt rename to data/src/androidTest/kotlin/com/afollestad/nocknock/data/TestUtil.kt diff --git a/data/src/main/java/com/afollestad/nocknock/data/AppDatabase.kt b/data/src/main/kotlin/com/afollestad/nocknock/data/AppDatabase.kt similarity index 100% rename from data/src/main/java/com/afollestad/nocknock/data/AppDatabase.kt rename to data/src/main/kotlin/com/afollestad/nocknock/data/AppDatabase.kt diff --git a/data/src/main/java/com/afollestad/nocknock/data/SiteDao.kt b/data/src/main/kotlin/com/afollestad/nocknock/data/SiteDao.kt similarity index 100% rename from data/src/main/java/com/afollestad/nocknock/data/SiteDao.kt rename to data/src/main/kotlin/com/afollestad/nocknock/data/SiteDao.kt diff --git a/data/src/main/java/com/afollestad/nocknock/data/SiteSettingsDao.kt b/data/src/main/kotlin/com/afollestad/nocknock/data/SiteSettingsDao.kt similarity index 100% rename from data/src/main/java/com/afollestad/nocknock/data/SiteSettingsDao.kt rename to data/src/main/kotlin/com/afollestad/nocknock/data/SiteSettingsDao.kt diff --git a/data/src/main/java/com/afollestad/nocknock/data/ValidationResultsDao.kt b/data/src/main/kotlin/com/afollestad/nocknock/data/ValidationResultsDao.kt similarity index 100% rename from data/src/main/java/com/afollestad/nocknock/data/ValidationResultsDao.kt rename to data/src/main/kotlin/com/afollestad/nocknock/data/ValidationResultsDao.kt diff --git a/data/src/main/java/com/afollestad/nocknock/data/model/Converters.kt b/data/src/main/kotlin/com/afollestad/nocknock/data/model/Converters.kt similarity index 100% rename from data/src/main/java/com/afollestad/nocknock/data/model/Converters.kt rename to data/src/main/kotlin/com/afollestad/nocknock/data/model/Converters.kt diff --git a/data/src/main/java/com/afollestad/nocknock/data/model/Site.kt b/data/src/main/kotlin/com/afollestad/nocknock/data/model/Site.kt similarity index 100% rename from data/src/main/java/com/afollestad/nocknock/data/model/Site.kt rename to data/src/main/kotlin/com/afollestad/nocknock/data/model/Site.kt diff --git a/data/src/main/java/com/afollestad/nocknock/data/model/SiteSettings.kt b/data/src/main/kotlin/com/afollestad/nocknock/data/model/SiteSettings.kt similarity index 100% rename from data/src/main/java/com/afollestad/nocknock/data/model/SiteSettings.kt rename to data/src/main/kotlin/com/afollestad/nocknock/data/model/SiteSettings.kt diff --git a/data/src/main/java/com/afollestad/nocknock/data/model/Status.kt b/data/src/main/kotlin/com/afollestad/nocknock/data/model/Status.kt similarity index 100% rename from data/src/main/java/com/afollestad/nocknock/data/model/Status.kt rename to data/src/main/kotlin/com/afollestad/nocknock/data/model/Status.kt diff --git a/data/src/main/java/com/afollestad/nocknock/data/model/ValidationMode.kt b/data/src/main/kotlin/com/afollestad/nocknock/data/model/ValidationMode.kt similarity index 100% rename from data/src/main/java/com/afollestad/nocknock/data/model/ValidationMode.kt rename to data/src/main/kotlin/com/afollestad/nocknock/data/model/ValidationMode.kt diff --git a/data/src/main/java/com/afollestad/nocknock/data/model/ValidationResult.kt b/data/src/main/kotlin/com/afollestad/nocknock/data/model/ValidationResult.kt similarity index 100% rename from data/src/main/java/com/afollestad/nocknock/data/model/ValidationResult.kt rename to data/src/main/kotlin/com/afollestad/nocknock/data/model/ValidationResult.kt diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/EngineModule.kt b/engine/src/main/kotlin/com/afollestad/nocknock/engine/EngineModule.kt similarity index 100% rename from engine/src/main/java/com/afollestad/nocknock/engine/EngineModule.kt rename to engine/src/main/kotlin/com/afollestad/nocknock/engine/EngineModule.kt diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/validation/BootReceiver.kt b/engine/src/main/kotlin/com/afollestad/nocknock/engine/validation/BootReceiver.kt similarity index 100% rename from engine/src/main/java/com/afollestad/nocknock/engine/validation/BootReceiver.kt rename to engine/src/main/kotlin/com/afollestad/nocknock/engine/validation/BootReceiver.kt diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/validation/ValidationJob.kt b/engine/src/main/kotlin/com/afollestad/nocknock/engine/validation/ValidationJob.kt similarity index 100% rename from engine/src/main/java/com/afollestad/nocknock/engine/validation/ValidationJob.kt rename to engine/src/main/kotlin/com/afollestad/nocknock/engine/validation/ValidationJob.kt diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/validation/ValidationManager.kt b/engine/src/main/kotlin/com/afollestad/nocknock/engine/validation/ValidationManager.kt similarity index 100% rename from engine/src/main/java/com/afollestad/nocknock/engine/validation/ValidationManager.kt rename to engine/src/main/kotlin/com/afollestad/nocknock/engine/validation/ValidationManager.kt diff --git a/engine/src/test/java/com/afollestad/nocknock/engine/CheckStatusManagerTest.kt b/engine/src/test/kotlin/com/afollestad/nocknock/engine/CheckStatusManagerTest.kt similarity index 100% rename from engine/src/test/java/com/afollestad/nocknock/engine/CheckStatusManagerTest.kt rename to engine/src/test/kotlin/com/afollestad/nocknock/engine/CheckStatusManagerTest.kt diff --git a/engine/src/test/java/com/afollestad/nocknock/engine/TestUtil.kt b/engine/src/test/kotlin/com/afollestad/nocknock/engine/TestUtil.kt similarity index 100% rename from engine/src/test/java/com/afollestad/nocknock/engine/TestUtil.kt rename to engine/src/test/kotlin/com/afollestad/nocknock/engine/TestUtil.kt diff --git a/notifications/src/main/java/com/afollestad/nocknock/notifications/Channel.kt b/notifications/src/main/kotlin/com/afollestad/nocknock/notifications/Channel.kt similarity index 100% rename from notifications/src/main/java/com/afollestad/nocknock/notifications/Channel.kt rename to notifications/src/main/kotlin/com/afollestad/nocknock/notifications/Channel.kt diff --git a/notifications/src/main/java/com/afollestad/nocknock/notifications/NockNotificationManager.kt b/notifications/src/main/kotlin/com/afollestad/nocknock/notifications/NockNotificationManager.kt similarity index 100% rename from notifications/src/main/java/com/afollestad/nocknock/notifications/NockNotificationManager.kt rename to notifications/src/main/kotlin/com/afollestad/nocknock/notifications/NockNotificationManager.kt diff --git a/notifications/src/main/java/com/afollestad/nocknock/notifications/NotificationsModule.kt b/notifications/src/main/kotlin/com/afollestad/nocknock/notifications/NotificationsModule.kt similarity index 100% rename from notifications/src/main/java/com/afollestad/nocknock/notifications/NotificationsModule.kt rename to notifications/src/main/kotlin/com/afollestad/nocknock/notifications/NotificationsModule.kt diff --git a/notifications/src/test/java/com/afollestad/nocknock/notifications/NockNotificationManagerTest.kt b/notifications/src/test/kotlin/com/afollestad/nocknock/notifications/NockNotificationManagerTest.kt similarity index 100% rename from notifications/src/test/java/com/afollestad/nocknock/notifications/NockNotificationManagerTest.kt rename to notifications/src/test/kotlin/com/afollestad/nocknock/notifications/NockNotificationManagerTest.kt diff --git a/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/ext/LiveDataExt.kt b/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/ext/LiveDataExt.kt deleted file mode 100644 index 7b73308..0000000 --- a/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/ext/LiveDataExt.kt +++ /dev/null @@ -1,148 +0,0 @@ -/** - * 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.ext - -import android.view.View -import android.widget.EditText -import android.widget.Spinner -import android.widget.TextView -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer -import androidx.lifecycle.Transformations -import com.afollestad.nocknock.utilities.ext.onTextChanged - -fun LiveData.map(mapper: (X) -> Y) = - Transformations.map(this, mapper)!! - -fun LiveData.switchMap(mapper: (X) -> LiveData) = - Transformations.switchMap(this, mapper)!! - -inline fun EditText.attachLiveData( - lifecycleOwner: LifecycleOwner, - data: MutableLiveData -) { - // Out - when { - T::class == Int::class -> { - onTextChanged { data.postValue(it.trim().toInt() as T) } - } - T::class == Long::class -> { - onTextChanged { data.postValue(it.trim().toLong() as T) } - } - T::class == String::class -> { - onTextChanged { data.postValue(it.trim() as T) } - } - else -> { - throw IllegalArgumentException("Can't send EditText text changes into ${T::class}") - } - } - // In - data.observe(lifecycleOwner, Observer { - when { - T::class == Int::class -> setText(it as Int) - T::class == String::class -> setText(it as String) - } - }) -} - -fun Spinner.attachLiveData( - lifecycleOwner: LifecycleOwner, - data: MutableLiveData, - outTransformer: (Int) -> T, - inTransformer: (T) -> Int -) { - // Out - onItemSelected { data.postValue(outTransformer(it)) } - // In - data.observe(lifecycleOwner, Observer { - setSelection(inTransformer(it)) - }) -} - -fun LiveData.toViewError( - owner: LifecycleOwner, - view: EditText -) = observe(owner, Observer { error -> - view.error = if (error != null) { - view.resources.getString(error) - } else { - null - } -}) - -inline fun LiveData.toViewText( - owner: LifecycleOwner, - view: TextView -) = observe(owner, Observer { - when { - T::class == Int::class -> view.setText(it as Int) - T::class == String::class -> view.text = it as String - else -> throw IllegalStateException("Cannot set ${T::class} as view text.") - } -}) - -fun LiveData.toViewVisibility( - owner: LifecycleOwner, - view: View -) = observe(owner, Observer { view.showOrHide(it) }) - -/** @author Aidan Follestad (@afollestad) */ -class ZipLiveData( - source1: LiveData, - source2: LiveData -) : MediatorLiveData>() { - - private var data1: T? = null - private var data2: K? = null - - init { - super.addSource(source1) { - data1 = it - maybeNotify() - } - super.addSource(source2) { - data2 = it - maybeNotify() - } - } - - private fun maybeNotify() { - if (data1 != null && data2 != null) { - value = Pair(data1!!, data2!!) - } - } - - override fun addSource( - source: LiveData, - onChanged: Observer - ) { - throw UnsupportedOperationException() - } - - override fun removeSource(toRemote: LiveData) { - throw UnsupportedOperationException() - } -} - -fun zip( - source1: LiveData, - source2: LiveData -): MediatorLiveData> { - return ZipLiveData(source1, source2) -} diff --git a/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/interval/CheckIntervalLayout.kt b/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/interval/CheckIntervalLayout.kt deleted file mode 100644 index 819f45d..0000000 --- a/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/interval/CheckIntervalLayout.kt +++ /dev/null @@ -1,133 +0,0 @@ -/** - * 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.interval - -import android.content.Context -import android.util.AttributeSet -import android.widget.ArrayAdapter -import android.widget.LinearLayout -import androidx.lifecycle.Lifecycle.State.DESTROYED -import androidx.lifecycle.Lifecycle.State.RESUMED -import androidx.lifecycle.Lifecycle.State.STARTED -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LifecycleRegistry -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer -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.utilities.ext.onTextChanged -import com.afollestad.nocknock.viewcomponents.R.array -import com.afollestad.nocknock.viewcomponents.R.layout -import com.afollestad.nocknock.viewcomponents.ext.onItemSelected -import kotlinx.android.synthetic.main.check_interval_layout.view.input -import kotlinx.android.synthetic.main.check_interval_layout.view.spinner - -/** @author Aidan Follestad (@afollestad) */ -class CheckIntervalLayout( - context: Context, - attrs: AttributeSet? = null -) : LinearLayout(context, attrs), LifecycleOwner { - - companion object { - const val INDEX_MINUTE = 0 - const val INDEX_HOUR = 1 - const val INDEX_DAY = 2 - const val INDEX_WEEK = 3 - } - - init { - orientation = VERTICAL - inflate(context, layout.check_interval_layout, this) - } - - private lateinit var valueData: MutableLiveData - private lateinit var multiplierData: MutableLiveData - - private val lifecycle = LifecycleRegistry(this) - - override fun onFinishInflate() { - super.onFinishInflate() - val spinnerAdapter = ArrayAdapter( - context, - layout.list_item_spinner, - resources.getStringArray(array.interval_options) - ) - spinnerAdapter.setDropDownViewResource( - layout.list_item_spinner_dropdown - ) - spinner.adapter = spinnerAdapter - lifecycle.markState(STARTED) - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - lifecycle.markState(RESUMED) - } - - override fun onDetachedFromWindow() { - lifecycle.markState(DESTROYED) - super.onDetachedFromWindow() - } - - fun setError(error: String?) { - input.error = error - } - - fun attach( - valueData: MutableLiveData, - multiplierData: MutableLiveData, - errorData: LiveData - ) { - this.valueData = valueData - this.multiplierData = multiplierData - - this.valueData.observe(this, Observer { - input.setText("$it") - }) - this.multiplierData.observe(this, Observer { multiplier -> - val targetPos = when (multiplier) { - MINUTE -> 0 - HOUR -> 1 - DAY -> 2 - WEEK -> 3 - else -> throw IllegalStateException("Unknown multiplier: $multiplier") - } - if (spinner.selectedItemPosition != targetPos) { - spinner.setSelection(targetPos) - } - }) - - errorData.observe(this, Observer { - setError(if (it != null) resources.getString(it) else null) - }) - - input.onTextChanged { this.valueData.value = it.toInt() } - spinner.onItemSelected { - this.multiplierData.value = when (it) { - INDEX_MINUTE -> MINUTE - INDEX_HOUR -> HOUR - INDEX_DAY -> DAY - INDEX_WEEK -> WEEK - else -> throw IllegalStateException("Unknown multiplier index: $it") - } - } - } - - override fun getLifecycle() = lifecycle -} diff --git a/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/LoadingIndicatorFrame.kt b/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/LoadingIndicatorFrame.kt similarity index 100% rename from viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/LoadingIndicatorFrame.kt rename to viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/LoadingIndicatorFrame.kt diff --git a/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/StatusImageView.kt b/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/StatusImageView.kt similarity index 100% rename from viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/StatusImageView.kt rename to viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/StatusImageView.kt diff --git a/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/ext/IntExt.kt b/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/ext/IntExt.kt similarity index 100% rename from viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/ext/IntExt.kt rename to viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/ext/IntExt.kt diff --git a/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/ext/ScrollViewExt.kt b/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/ext/ScrollViewExt.kt similarity index 100% rename from viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/ext/ScrollViewExt.kt rename to viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/ext/ScrollViewExt.kt diff --git a/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/ext/SpinnerExt.kt b/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/ext/SpinnerExt.kt similarity index 100% rename from viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/ext/SpinnerExt.kt rename to viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/ext/SpinnerExt.kt diff --git a/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/ext/ViewExt.kt b/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/ext/ViewExt.kt similarity index 100% rename from viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/ext/ViewExt.kt rename to viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/ext/ViewExt.kt diff --git a/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/interval/ValidationIntervalLayout.kt b/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/interval/ValidationIntervalLayout.kt new file mode 100644 index 0000000..58bbe9d --- /dev/null +++ b/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/interval/ValidationIntervalLayout.kt @@ -0,0 +1,101 @@ +/** + * 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.interval + +import android.content.Context +import android.util.AttributeSet +import android.widget.ArrayAdapter +import android.widget.LinearLayout +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +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.livedata.attachLiveData +import com.afollestad.nocknock.viewcomponents.livedata.lifecycleOwner +import com.afollestad.nocknock.viewcomponents.livedata.toViewError +import kotlinx.android.synthetic.main.validation_interval_layout.view.input +import kotlinx.android.synthetic.main.validation_interval_layout.view.spinner + +/** @author Aidan Follestad (@afollestad) */ +class ValidationIntervalLayout( + context: Context, + attrs: AttributeSet? = null +) : LinearLayout(context, attrs) { + + companion object { + const val INDEX_MINUTE = 0 + const val INDEX_HOUR = 1 + const val INDEX_DAY = 2 + const val INDEX_WEEK = 3 + } + + init { + orientation = VERTICAL + inflate(context, layout.validation_interval_layout, this) + } + + override fun onFinishInflate() { + super.onFinishInflate() + val spinnerAdapter = ArrayAdapter( + context, + layout.list_item_spinner, + resources.getStringArray(array.interval_options) + ) + spinnerAdapter.setDropDownViewResource( + layout.list_item_spinner_dropdown + ) + spinner.adapter = spinnerAdapter + } + + fun attach( + valueData: MutableLiveData, + multiplierData: MutableLiveData, + errorData: LiveData + ) { + input.attachLiveData(lifecycleOwner(), valueData) + spinner.attachLiveData( + lifecycleOwner = lifecycleOwner(), + data = multiplierData, + inTransformer = { + when (it) { + MINUTE -> 0 + HOUR -> 1 + DAY -> 2 + WEEK -> 3 + else -> throw IllegalStateException("Unknown multiplier: $it") + } + }, + outTransformer = { + when (it) { + INDEX_MINUTE -> MINUTE + INDEX_HOUR -> HOUR + INDEX_DAY -> DAY + INDEX_WEEK -> WEEK + else -> throw IllegalStateException("Unknown multiplier index: $it") + } + } + ) + errorData.toViewError(lifecycleOwner(), this, ::setError) + } + + private fun setError(error: String?) { + input.error = error + } +} diff --git a/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/js/JavaScriptInputLayout.kt b/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/js/JavaScriptInputLayout.kt similarity index 58% rename from viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/js/JavaScriptInputLayout.kt rename to viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/js/JavaScriptInputLayout.kt index 3c91995..cf7858c 100644 --- a/viewcomponents/src/main/java/com/afollestad/nocknock/viewcomponents/js/JavaScriptInputLayout.kt +++ b/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/js/JavaScriptInputLayout.kt @@ -18,22 +18,17 @@ package com.afollestad.nocknock.viewcomponents.js import android.content.Context import android.util.AttributeSet import android.widget.HorizontalScrollView -import androidx.lifecycle.Lifecycle.State.CREATED -import androidx.lifecycle.Lifecycle.State.DESTROYED -import androidx.lifecycle.Lifecycle.State.RESUMED -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer -import com.afollestad.nocknock.utilities.ext.onTextChanged import com.afollestad.nocknock.viewcomponents.R.dimen import com.afollestad.nocknock.viewcomponents.R.layout -import com.afollestad.nocknock.viewcomponents.R.string import com.afollestad.nocknock.viewcomponents.ext.dimenFloat import com.afollestad.nocknock.viewcomponents.ext.dimenInt import com.afollestad.nocknock.viewcomponents.ext.showOrHide -import com.afollestad.nocknock.viewcomponents.ext.toViewVisibility +import com.afollestad.nocknock.viewcomponents.livedata.attachLiveData +import com.afollestad.nocknock.viewcomponents.livedata.lifecycleOwner +import com.afollestad.nocknock.viewcomponents.livedata.toViewError +import com.afollestad.nocknock.viewcomponents.livedata.toViewVisibility import kotlinx.android.synthetic.main.javascript_input_layout.view.error_text import kotlinx.android.synthetic.main.javascript_input_layout.view.userInput @@ -41,10 +36,7 @@ import kotlinx.android.synthetic.main.javascript_input_layout.view.userInput class JavaScriptInputLayout( context: Context, attrs: AttributeSet? = null -) : HorizontalScrollView(context, attrs), LifecycleOwner { - - private val lifecycle = LifecycleRegistry(this) - private lateinit var codeData: MutableLiveData +) : HorizontalScrollView(context, attrs) { init { val contentInset = dimenInt(dimen.content_inset) @@ -61,51 +53,20 @@ class JavaScriptInputLayout( inflate(context, layout.javascript_input_layout, this) } - override fun onFinishInflate() { - super.onFinishInflate() - lifecycle.markState(CREATED) - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - lifecycle.markState(RESUMED) - } - - override fun onDetachedFromWindow() { - lifecycle.markState(DESTROYED) - super.onDetachedFromWindow() - } - - fun setError(error: String?) { - error_text.showOrHide(error != null) - error_text.text = error - } - fun attach( codeData: MutableLiveData, errorData: LiveData, visibility: LiveData ) { - this.codeData = codeData - this.codeData.observe(this, Observer { - if (it.isNullOrEmpty()) { - setDefaultCode() - } else { - userInput.setText(it) - } - }) - errorData.observe(this, Observer { - setError(if (it != null) resources.getString(it) else null) - }) - visibility.toViewVisibility(this, this) - userInput.onTextChanged { this.codeData.value = it } + userInput.attachLiveData(lifecycleOwner(), codeData) + errorData.toViewError(lifecycleOwner(), this, ::setError) + visibility.toViewVisibility(lifecycleOwner(), this) } fun clear() = userInput.setText("") - private fun setDefaultCode() = userInput.setText( - string.default_js - ) - - override fun getLifecycle() = lifecycle + private fun setError(error: String?) { + error_text.showOrHide(error != null) + error_text.text = error + } } diff --git a/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/livedata/LiveDataViewExt.kt b/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/livedata/LiveDataViewExt.kt new file mode 100644 index 0000000..e27be55 --- /dev/null +++ b/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/livedata/LiveDataViewExt.kt @@ -0,0 +1,120 @@ +/** + * 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.livedata + +import android.view.View +import android.widget.EditText +import android.widget.Spinner +import android.widget.TextView +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import com.afollestad.nocknock.utilities.ext.onTextChanged +import com.afollestad.nocknock.utilities.ext.setTextAndMaintainSelection +import com.afollestad.nocknock.utilities.livedata.distinct +import com.afollestad.nocknock.viewcomponents.ext.onItemSelected +import com.afollestad.nocknock.viewcomponents.ext.showOrHide + +inline fun EditText.attachLiveData( + lifecycleOwner: LifecycleOwner, + data: MutableLiveData, + debounce: Int = 100, + pushOutChanges: Boolean = true, + pullInChanges: Boolean = false +) { + // Initial value + if (T::class == String::class) { + data.value = this.text.trim().toString() as T + } + // Out + if (pushOutChanges) { + when { + T::class == Int::class -> { + onTextChanged(debounce) { data.postValue(it.trim().toInt() as T) } + } + T::class == Long::class -> { + onTextChanged(debounce) { data.postValue(it.trim().toLong() as T) } + } + T::class == String::class -> { + onTextChanged(debounce) { data.postValue(it as T) } + } + else -> { + throw IllegalArgumentException("Can't send EditText text changes into ${T::class}") + } + } + } + // In + if (pullInChanges) { + data.distinct() + .observe(lifecycleOwner, Observer { + when { + T::class == Int::class -> setText(it as Int) + T::class == String::class -> setTextAndMaintainSelection(it as String) + } + }) + } +} + +fun Spinner.attachLiveData( + lifecycleOwner: LifecycleOwner, + data: MutableLiveData, + outTransformer: (Int) -> T, + inTransformer: (T) -> Int +) { + // Out + onItemSelected { data.postValue(outTransformer(it)) } + // In + data.distinct() + .observe(lifecycleOwner, Observer { + setSelection(inTransformer(it)) + }) +} + +fun LiveData.toViewError( + owner: LifecycleOwner, + view: View, + setter: (String?) -> Unit +) = observe(owner, Observer { error -> + setter( + if (error != null) { + view.resources.getString(error) + } else { + null + } + ) +}) + +fun LiveData.toViewError( + owner: LifecycleOwner, + view: EditText +) = toViewError(owner, view, view::setError) + +fun LiveData<*>.toViewText( + owner: LifecycleOwner, + view: TextView +) = distinct().observe(owner, Observer { + when (it) { + is Int -> view.setText(it) + is String -> view.text = it + else -> throw IllegalStateException("Can't set $it to a text view.") + } +}) + +fun LiveData.toViewVisibility( + owner: LifecycleOwner, + view: View +) = distinct().observe(owner, Observer { view.showOrHide(it) }) diff --git a/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/livedata/ViewLifecycleOwner.kt b/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/livedata/ViewLifecycleOwner.kt new file mode 100644 index 0000000..111a36f --- /dev/null +++ b/viewcomponents/src/main/kotlin/com/afollestad/nocknock/viewcomponents/livedata/ViewLifecycleOwner.kt @@ -0,0 +1,59 @@ +/** + * 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.livedata + +import android.view.View +import android.view.View.OnAttachStateChangeListener +import androidx.lifecycle.Lifecycle.State.CREATED +import androidx.lifecycle.Lifecycle.State.DESTROYED +import androidx.lifecycle.Lifecycle.State.RESUMED +import androidx.lifecycle.Lifecycle.State.STARTED +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import com.afollestad.nocknock.viewcomponents.R + +/** @author Aidan Follestad (@afollestad) */ +class ViewLifecycleOwner(view: View) : LifecycleOwner, OnAttachStateChangeListener { + + private val lifecycleRegistry = LifecycleRegistry(this) + + init { + view.addOnAttachStateChangeListener(this) + } + + override fun getLifecycle() = lifecycleRegistry + + override fun onViewAttachedToWindow(v: View?) { + lifecycleRegistry.markState(CREATED) + lifecycleRegistry.markState(STARTED) + lifecycleRegistry.markState(RESUMED) + } + + override fun onViewDetachedFromWindow(v: View) { + lifecycleRegistry.markState(DESTROYED) + } +} + +fun View.lifecycleOwner(): LifecycleOwner { + val tagOwner = getTag(R.id.view_lifecycle_registry) as? ViewLifecycleOwner + return if (tagOwner != null) { + tagOwner + } else { + val newOwner = ViewLifecycleOwner(this) + setTag(R.id.view_lifecycle_registry, newOwner) + newOwner + } +} diff --git a/viewcomponents/src/main/res/layout/check_interval_layout.xml b/viewcomponents/src/main/res/layout/validation_interval_layout.xml similarity index 100% rename from viewcomponents/src/main/res/layout/check_interval_layout.xml rename to viewcomponents/src/main/res/layout/validation_interval_layout.xml diff --git a/viewcomponents/src/main/res/values/ids.xml b/viewcomponents/src/main/res/values/ids.xml new file mode 100644 index 0000000..561aa8e --- /dev/null +++ b/viewcomponents/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + +