mirror of
https://github.com/afollestad/nock-nock.git
synced 2025-08-08 08:58:41 +00:00
Move majority of add site logic to a presenter, add unit tests
This commit is contained in:
parent
f87e1438d2
commit
225434b41a
21 changed files with 622 additions and 155 deletions
|
@ -18,7 +18,7 @@
|
||||||
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
|
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.afollestad.nocknock.ui.MainActivity"
|
android:name="com.afollestad.nocknock.ui.main.MainActivity"
|
||||||
android:launchMode="singleTop">
|
android:launchMode="singleTop">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
@ -28,14 +28,14 @@
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.afollestad.nocknock.ui.AddSiteActivity"
|
android:name="com.afollestad.nocknock.ui.addsite.AddSiteActivity"
|
||||||
android:label="@string/add_site"
|
android:label="@string/add_site"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/AppTheme.Transparent"
|
android:theme="@style/AppTheme.Transparent"
|
||||||
android:windowSoftInputMode="stateHidden"/>
|
android:windowSoftInputMode="stateHidden"/>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.afollestad.nocknock.ui.ViewSiteActivity"
|
android:name="com.afollestad.nocknock.ui.viewsite.ViewSiteActivity"
|
||||||
android:label="@string/view_site"
|
android:label="@string/view_site"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/AppTheme.Ink"
|
android:theme="@style/AppTheme.Ink"
|
||||||
|
|
|
@ -12,9 +12,9 @@ import com.afollestad.nocknock.di.DaggerAppComponent
|
||||||
import com.afollestad.nocknock.engine.statuscheck.BootReceiver
|
import com.afollestad.nocknock.engine.statuscheck.BootReceiver
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob
|
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob
|
||||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.ui.AddSiteActivity
|
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
||||||
import com.afollestad.nocknock.ui.MainActivity
|
import com.afollestad.nocknock.ui.main.MainActivity
|
||||||
import com.afollestad.nocknock.ui.ViewSiteActivity
|
import com.afollestad.nocknock.ui.viewsite.ViewSiteActivity
|
||||||
import com.afollestad.nocknock.utilities.Injector
|
import com.afollestad.nocknock.utilities.Injector
|
||||||
import com.afollestad.nocknock.utilities.ext.systemService
|
import com.afollestad.nocknock.utilities.ext.systemService
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
|
@ -110,10 +110,9 @@ class ServerAdapter(private val listener: Listener) : RecyclerView.Adapter<Serve
|
||||||
|
|
||||||
fun set(newModels: List<ServerModel>) {
|
fun set(newModels: List<ServerModel>) {
|
||||||
this.models.clear()
|
this.models.clear()
|
||||||
if (newModels.isEmpty()) {
|
if (!newModels.isEmpty()) {
|
||||||
return
|
this.models.addAll(newModels)
|
||||||
}
|
}
|
||||||
this.models.addAll(newModels)
|
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,9 @@ import com.afollestad.nocknock.engine.EngineModule
|
||||||
import com.afollestad.nocknock.engine.statuscheck.BootReceiver
|
import com.afollestad.nocknock.engine.statuscheck.BootReceiver
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob
|
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob
|
||||||
import com.afollestad.nocknock.notifications.NotificationsModule
|
import com.afollestad.nocknock.notifications.NotificationsModule
|
||||||
import com.afollestad.nocknock.ui.AddSiteActivity
|
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
||||||
import com.afollestad.nocknock.ui.MainActivity
|
import com.afollestad.nocknock.ui.main.MainActivity
|
||||||
import com.afollestad.nocknock.ui.ViewSiteActivity
|
import com.afollestad.nocknock.ui.viewsite.ViewSiteActivity
|
||||||
import com.afollestad.nocknock.utilities.UtilitiesModule
|
import com.afollestad.nocknock.utilities.UtilitiesModule
|
||||||
import dagger.BindsInstance
|
import dagger.BindsInstance
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
|
|
|
@ -5,8 +5,10 @@
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.di
|
package com.afollestad.nocknock.di
|
||||||
|
|
||||||
import com.afollestad.nocknock.presenters.MainPresenter
|
import com.afollestad.nocknock.ui.addsite.AddSitePresenter
|
||||||
import com.afollestad.nocknock.presenters.RealMainPresenter
|
import com.afollestad.nocknock.ui.addsite.RealAddSitePresenter
|
||||||
|
import com.afollestad.nocknock.ui.main.MainPresenter
|
||||||
|
import com.afollestad.nocknock.ui.main.RealMainPresenter
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
@ -20,4 +22,10 @@ abstract class MainBindModule {
|
||||||
abstract fun provideMainPresenter(
|
abstract fun provideMainPresenter(
|
||||||
presenter: RealMainPresenter
|
presenter: RealMainPresenter
|
||||||
): MainPresenter
|
): MainPresenter
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun provideAddSitePresenter(
|
||||||
|
presenter: RealAddSitePresenter
|
||||||
|
): AddSitePresenter
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,35 +3,29 @@
|
||||||
*
|
*
|
||||||
* Designed and developed by Aidan Follestad (@afollestad)
|
* Designed and developed by Aidan Follestad (@afollestad)
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.ui
|
package com.afollestad.nocknock.ui.addsite
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION
|
import android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Patterns.WEB_URL
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewAnimationUtils.createCircularReveal
|
import android.view.ViewAnimationUtils.createCircularReveal
|
||||||
import android.view.animation.AccelerateInterpolator
|
import android.view.animation.AccelerateInterpolator
|
||||||
import android.view.animation.DecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.afollestad.nocknock.R
|
import com.afollestad.nocknock.R
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
|
||||||
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
|
||||||
import com.afollestad.nocknock.data.ValidationMode
|
import com.afollestad.nocknock.data.ValidationMode
|
||||||
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||||
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||||
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||||
import com.afollestad.nocknock.data.indexToValidationMode
|
import com.afollestad.nocknock.data.indexToValidationMode
|
||||||
import com.afollestad.nocknock.engine.db.ServerModelStore
|
import com.afollestad.nocknock.ui.main.MainActivity
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||||
import com.afollestad.nocknock.utilities.ext.injector
|
import com.afollestad.nocknock.utilities.ext.injector
|
||||||
import com.afollestad.nocknock.utilities.ext.onEnd
|
import com.afollestad.nocknock.utilities.ext.onEnd
|
||||||
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
|
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.conceal
|
import com.afollestad.nocknock.viewcomponents.ext.conceal
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.hide
|
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.onItemSelected
|
import com.afollestad.nocknock.viewcomponents.ext.onItemSelected
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.onLayout
|
import com.afollestad.nocknock.viewcomponents.ext.onLayout
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.show
|
import com.afollestad.nocknock.viewcomponents.ext.show
|
||||||
|
@ -42,20 +36,15 @@ import kotlinx.android.synthetic.main.activity_addsite.doneBtn
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.inputName
|
import kotlinx.android.synthetic.main.activity_addsite.inputName
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.inputUrl
|
import kotlinx.android.synthetic.main.activity_addsite.inputUrl
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.loadingProgress
|
import kotlinx.android.synthetic.main.activity_addsite.loadingProgress
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.nameTiLayout
|
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.responseValidationMode
|
import kotlinx.android.synthetic.main.activity_addsite.responseValidationMode
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchTerm
|
import kotlinx.android.synthetic.main.activity_addsite.responseValidationSearchTerm
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.rootView
|
import kotlinx.android.synthetic.main.activity_addsite.rootView
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout
|
import kotlinx.android.synthetic.main.activity_addsite.scriptInputLayout
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning
|
import kotlinx.android.synthetic.main.activity_addsite.textUrlWarning
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.toolbar
|
import kotlinx.android.synthetic.main.activity_addsite.toolbar
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.urlTiLayout
|
|
||||||
import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
|
import kotlinx.android.synthetic.main.activity_addsite.validationModeDescription
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.properties.Delegates.notNull
|
import kotlin.properties.Delegates.notNull
|
||||||
|
|
||||||
|
@ -76,7 +65,7 @@ fun MainActivity.intentToAdd(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @author Aidan Follestad (afollestad) */
|
/** @author Aidan Follestad (afollestad) */
|
||||||
class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
|
class AddSiteActivity : AppCompatActivity(), AddSiteView {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val REVEAL_DURATION = 300L
|
private const val REVEAL_DURATION = 300L
|
||||||
|
@ -84,8 +73,7 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
|
||||||
|
|
||||||
private var isClosing: Boolean = false
|
private var isClosing: Boolean = false
|
||||||
|
|
||||||
@Inject lateinit var serverModelStore: ServerModelStore
|
@Inject lateinit var presenter: AddSitePresenter
|
||||||
@Inject lateinit var checkStatusManager: CheckStatusManager
|
|
||||||
|
|
||||||
private var revealCx by notNull<Int>()
|
private var revealCx by notNull<Int>()
|
||||||
private var revealCy by notNull<Int>()
|
private var revealCy by notNull<Int>()
|
||||||
|
@ -94,9 +82,11 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
injector().injectInto(this)
|
|
||||||
|
|
||||||
|
injector().injectInto(this)
|
||||||
setContentView(R.layout.activity_addsite)
|
setContentView(R.layout.activity_addsite)
|
||||||
|
presenter.takeView(this)
|
||||||
|
|
||||||
toolbar.setNavigationOnClickListener { closeActivityWithReveal() }
|
toolbar.setNavigationOnClickListener { closeActivityWithReveal() }
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
|
@ -117,25 +107,7 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
inputUrl.setOnFocusChangeListener { _, hasFocus ->
|
inputUrl.setOnFocusChangeListener { _, hasFocus ->
|
||||||
if (!hasFocus) {
|
presenter.onUrlInputFocusChange(hasFocus, inputUrl.trimmedText())
|
||||||
val inputStr = inputUrl.text
|
|
||||||
.toString()
|
|
||||||
.trim()
|
|
||||||
if (inputStr.isEmpty()) {
|
|
||||||
return@setOnFocusChangeListener
|
|
||||||
}
|
|
||||||
|
|
||||||
val uri = Uri.parse(inputStr)
|
|
||||||
if (uri.scheme == null) {
|
|
||||||
inputUrl.setText("http://$inputStr")
|
|
||||||
textUrlWarning.hide()
|
|
||||||
} else if ("http" != uri.scheme && "https" != uri.scheme) {
|
|
||||||
textUrlWarning.show()
|
|
||||||
textUrlWarning.setText(R.string.warning_http_url)
|
|
||||||
} else {
|
|
||||||
textUrlWarning.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val validationOptionsAdapter = ArrayAdapter(
|
val validationOptionsAdapter = ArrayAdapter(
|
||||||
|
@ -146,23 +118,91 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
|
||||||
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
validationOptionsAdapter.setDropDownViewResource(R.layout.list_item_spinner_dropdown)
|
||||||
|
|
||||||
responseValidationMode.adapter = validationOptionsAdapter
|
responseValidationMode.adapter = validationOptionsAdapter
|
||||||
responseValidationMode.onItemSelected { pos ->
|
responseValidationMode.onItemSelected(presenter::onValidationModeSelected)
|
||||||
responseValidationSearchTerm.showOrHide(pos == 1)
|
|
||||||
scriptInputLayout.showOrHide(pos == 2)
|
|
||||||
|
|
||||||
validationModeDescription.setText(
|
doneBtn.setOnClickListener {
|
||||||
when (pos) {
|
val checkInterval = checkIntervalLayout.getSelectedCheckInterval()
|
||||||
0 -> R.string.validation_mode_status_desc
|
val validationMode =
|
||||||
1 -> R.string.validation_mode_term_desc
|
responseValidationMode.selectedItemPosition.indexToValidationMode()
|
||||||
2 -> R.string.validation_mode_javascript_desc
|
|
||||||
else -> throw IllegalStateException("Unknown validation mode position: $pos")
|
isClosing = true
|
||||||
}
|
presenter.commit(
|
||||||
|
name = inputName.trimmedText(),
|
||||||
|
url = inputUrl.trimmedText(),
|
||||||
|
checkInterval = checkInterval,
|
||||||
|
validationMode = validationMode,
|
||||||
|
validationContent = validationMode.validationContent()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
doneBtn.setOnClickListener(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
presenter.dropView()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setLoading() = loadingProgress.setLoading()
|
||||||
|
|
||||||
|
override fun setDoneLoading() = loadingProgress.setDone()
|
||||||
|
|
||||||
|
override fun showOrHideUrlSchemeWarning(show: Boolean) {
|
||||||
|
textUrlWarning.showOrHide(show)
|
||||||
|
if (show) {
|
||||||
|
textUrlWarning.setText(R.string.warning_http_url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showOrHideValidationSearchTerm(show: Boolean) =
|
||||||
|
responseValidationSearchTerm.showOrHide(show)
|
||||||
|
|
||||||
|
override fun showOrHideScriptInput(show: Boolean) = scriptInputLayout.showOrHide(show)
|
||||||
|
|
||||||
|
override fun setValidationModeDescription(res: Int) = validationModeDescription.setText(res)
|
||||||
|
|
||||||
|
override fun setInputErrors(errors: InputErrors) {
|
||||||
|
isClosing = false
|
||||||
|
inputName.error = if (errors.name != null) {
|
||||||
|
getString(errors.name!!)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
inputUrl.error = if (errors.url != null) {
|
||||||
|
getString(errors.url!!)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
checkIntervalLayout.setError(
|
||||||
|
if (errors.checkInterval != null) {
|
||||||
|
getString(errors.checkInterval!!)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
responseValidationSearchTerm.error = if (errors.termSearch != null) {
|
||||||
|
getString(errors.termSearch!!)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
scriptInputLayout.setError(
|
||||||
|
if (errors.javaScript != null) {
|
||||||
|
getString(errors.javaScript!!)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSiteAdded() {
|
||||||
|
setResult(RESULT_OK)
|
||||||
|
finish()
|
||||||
|
overridePendingTransition(R.anim.fade_out, R.anim.fade_out)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun scopeWhileAttached(
|
||||||
|
context: CoroutineContext,
|
||||||
|
exec: ScopeReceiver
|
||||||
|
) = rootView.scopeWhileAttached(context, exec)
|
||||||
|
|
||||||
private fun circularRevealActivity() {
|
private fun circularRevealActivity() {
|
||||||
val circularReveal =
|
val circularReveal =
|
||||||
createCircularReveal(rootView, revealCx, revealCy, 0f, revealRadius)
|
createCircularReveal(rootView, revealCx, revealCy, 0f, revealRadius)
|
||||||
|
@ -190,71 +230,6 @@ class AddSiteActivity : AppCompatActivity(), View.OnClickListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Done button
|
|
||||||
override fun onClick(view: View) {
|
|
||||||
isClosing = true
|
|
||||||
var newModel = ServerModel(
|
|
||||||
name = inputName.trimmedText(),
|
|
||||||
url = inputUrl.trimmedText(),
|
|
||||||
status = WAITING,
|
|
||||||
validationMode = STATUS_CODE
|
|
||||||
)
|
|
||||||
|
|
||||||
if (newModel.name.isEmpty()) {
|
|
||||||
nameTiLayout.error = getString(R.string.please_enter_name)
|
|
||||||
isClosing = false
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
nameTiLayout.error = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newModel.url.isEmpty()) {
|
|
||||||
urlTiLayout.error = getString(R.string.please_enter_url)
|
|
||||||
isClosing = false
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
urlTiLayout.error = null
|
|
||||||
if (!WEB_URL.matcher(newModel.url).find()) {
|
|
||||||
urlTiLayout.error = getString(R.string.please_enter_valid_url)
|
|
||||||
isClosing = false
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
val uri = Uri.parse(newModel.url)
|
|
||||||
if (uri.scheme == null) {
|
|
||||||
newModel = newModel.copy(url = "http://${newModel.url}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val selectedCheckInterval = checkIntervalLayout.getSelectedCheckInterval()
|
|
||||||
val selectedValidationMode =
|
|
||||||
responseValidationMode.selectedItemPosition.indexToValidationMode()
|
|
||||||
|
|
||||||
newModel = newModel.copy(
|
|
||||||
checkInterval = selectedCheckInterval,
|
|
||||||
validationMode = selectedValidationMode,
|
|
||||||
validationContent = selectedValidationMode.validationContent()
|
|
||||||
)
|
|
||||||
|
|
||||||
rootView.scopeWhileAttached(Main) {
|
|
||||||
launch(coroutineContext) {
|
|
||||||
loadingProgress.setLoading()
|
|
||||||
val storedModel = async(IO) { serverModelStore.put(newModel) }.await()
|
|
||||||
|
|
||||||
checkStatusManager.scheduleCheck(
|
|
||||||
site = storedModel,
|
|
||||||
rightNow = true,
|
|
||||||
cancelPrevious = true
|
|
||||||
)
|
|
||||||
loadingProgress.setDone()
|
|
||||||
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
finish()
|
|
||||||
overridePendingTransition(R.anim.fade_out, R.anim.fade_out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() = closeActivityWithReveal()
|
override fun onBackPressed() = closeActivityWithReveal()
|
||||||
|
|
||||||
private fun ValidationMode.validationContent() = when (this) {
|
private fun ValidationMode.validationContent() = when (this) {
|
|
@ -0,0 +1,167 @@
|
||||||
|
/*
|
||||||
|
* Licensed under Apache-2.0
|
||||||
|
*
|
||||||
|
* Designed and developed by Aidan Follestad (@afollestad)
|
||||||
|
*/
|
||||||
|
package com.afollestad.nocknock.ui.addsite
|
||||||
|
|
||||||
|
import androidx.annotation.CheckResult
|
||||||
|
import com.afollestad.nocknock.R
|
||||||
|
import com.afollestad.nocknock.data.ServerModel
|
||||||
|
import com.afollestad.nocknock.data.ServerStatus.WAITING
|
||||||
|
import com.afollestad.nocknock.data.ValidationMode
|
||||||
|
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||||
|
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||||
|
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||||
|
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||||
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/** @author Aidan Follestad (afollestad) */
|
||||||
|
data class InputErrors(
|
||||||
|
var name: Int? = null,
|
||||||
|
var url: Int? = null,
|
||||||
|
var checkInterval: Int? = null,
|
||||||
|
var termSearch: Int? = null,
|
||||||
|
var javaScript: Int? = null
|
||||||
|
) {
|
||||||
|
@CheckResult fun any(): Boolean {
|
||||||
|
return name != null || url != null || checkInterval != null ||
|
||||||
|
termSearch != null || javaScript != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @author Aidan Follestad (afollestad) */
|
||||||
|
interface AddSitePresenter {
|
||||||
|
|
||||||
|
fun takeView(view: AddSiteView)
|
||||||
|
|
||||||
|
fun onUrlInputFocusChange(
|
||||||
|
focused: Boolean,
|
||||||
|
content: String
|
||||||
|
)
|
||||||
|
|
||||||
|
fun onValidationModeSelected(index: Int)
|
||||||
|
|
||||||
|
fun commit(
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
checkInterval: Long,
|
||||||
|
validationMode: ValidationMode,
|
||||||
|
validationContent: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
fun dropView()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @author Aidan Follestad (afollestad) */
|
||||||
|
class RealAddSitePresenter @Inject constructor(
|
||||||
|
private val serverModelStore: ServerModelStore,
|
||||||
|
private val checkStatusManager: CheckStatusManager
|
||||||
|
) : AddSitePresenter {
|
||||||
|
|
||||||
|
private var view: AddSiteView? = null
|
||||||
|
|
||||||
|
override fun takeView(view: AddSiteView) {
|
||||||
|
this.view = view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUrlInputFocusChange(
|
||||||
|
focused: Boolean,
|
||||||
|
content: String
|
||||||
|
) {
|
||||||
|
if (content.isEmpty() || focused) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val url = HttpUrl.parse(content)
|
||||||
|
if (url == null ||
|
||||||
|
(url.scheme() != "http" &&
|
||||||
|
url.scheme() != "https")
|
||||||
|
) {
|
||||||
|
view?.showOrHideUrlSchemeWarning(true)
|
||||||
|
} else {
|
||||||
|
view?.showOrHideUrlSchemeWarning(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onValidationModeSelected(index: Int) = with(view!!) {
|
||||||
|
showOrHideValidationSearchTerm(index == 1)
|
||||||
|
showOrHideScriptInput(index == 2)
|
||||||
|
setValidationModeDescription(
|
||||||
|
when (index) {
|
||||||
|
0 -> R.string.validation_mode_status_desc
|
||||||
|
1 -> R.string.validation_mode_term_desc
|
||||||
|
2 -> R.string.validation_mode_javascript_desc
|
||||||
|
else -> throw IllegalStateException("Unknown validation mode position: $index")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun commit(
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
checkInterval: Long,
|
||||||
|
validationMode: ValidationMode,
|
||||||
|
validationContent: String?
|
||||||
|
) {
|
||||||
|
val inputErrors = InputErrors()
|
||||||
|
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
inputErrors.name = R.string.please_enter_name
|
||||||
|
}
|
||||||
|
if (url.isEmpty()) {
|
||||||
|
inputErrors.url = R.string.please_enter_url
|
||||||
|
} else if (HttpUrl.parse(url) == null) {
|
||||||
|
inputErrors.url = R.string.please_enter_valid_url
|
||||||
|
}
|
||||||
|
if (checkInterval <= 0) {
|
||||||
|
inputErrors.checkInterval = R.string.please_enter_check_interval
|
||||||
|
}
|
||||||
|
if (validationMode == TERM_SEARCH && validationContent.isNullOrEmpty()) {
|
||||||
|
inputErrors.termSearch = R.string.please_enter_search_term
|
||||||
|
} else if (validationMode == JAVASCRIPT && validationContent.isNullOrEmpty()) {
|
||||||
|
inputErrors.javaScript = R.string.please_enter_javaScript
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputErrors.any()) {
|
||||||
|
view?.setInputErrors(inputErrors)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val newModel = ServerModel(
|
||||||
|
name = name,
|
||||||
|
url = url,
|
||||||
|
status = WAITING,
|
||||||
|
checkInterval = checkInterval,
|
||||||
|
validationMode = validationMode,
|
||||||
|
validationContent = validationContent
|
||||||
|
)
|
||||||
|
|
||||||
|
with(view!!) {
|
||||||
|
scopeWhileAttached(Main) {
|
||||||
|
launch(coroutineContext) {
|
||||||
|
setLoading()
|
||||||
|
val storedModel = async(IO) {
|
||||||
|
serverModelStore.put(newModel)
|
||||||
|
}.await()
|
||||||
|
|
||||||
|
checkStatusManager.scheduleCheck(
|
||||||
|
site = storedModel,
|
||||||
|
rightNow = true,
|
||||||
|
cancelPrevious = true
|
||||||
|
)
|
||||||
|
setDoneLoading()
|
||||||
|
onSiteAdded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dropView() {
|
||||||
|
view = null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Licensed under Apache-2.0
|
||||||
|
*
|
||||||
|
* Designed and developed by Aidan Follestad (@afollestad)
|
||||||
|
*/
|
||||||
|
package com.afollestad.nocknock.ui.addsite
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
/** @author Aidan Follestad (afollestad) */
|
||||||
|
interface AddSiteView {
|
||||||
|
|
||||||
|
fun setLoading()
|
||||||
|
|
||||||
|
fun setDoneLoading()
|
||||||
|
|
||||||
|
fun showOrHideUrlSchemeWarning(show: Boolean)
|
||||||
|
|
||||||
|
fun showOrHideValidationSearchTerm(show: Boolean)
|
||||||
|
|
||||||
|
fun showOrHideScriptInput(show: Boolean)
|
||||||
|
|
||||||
|
fun setValidationModeDescription(@StringRes res: Int)
|
||||||
|
|
||||||
|
fun setInputErrors(errors: InputErrors)
|
||||||
|
|
||||||
|
fun onSiteAdded()
|
||||||
|
|
||||||
|
fun scopeWhileAttached(
|
||||||
|
context: CoroutineContext,
|
||||||
|
exec: ScopeReceiver
|
||||||
|
)
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* Designed and developed by Aidan Follestad (@afollestad)
|
* Designed and developed by Aidan Follestad (@afollestad)
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.ui
|
package com.afollestad.nocknock.ui.main
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.ActivityOptions.makeSceneTransitionAnimation
|
import android.app.ActivityOptions.makeSceneTransitionAnimation
|
||||||
|
@ -25,8 +25,8 @@ import com.afollestad.nocknock.adapter.ServerAdapter
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.ServerModel
|
||||||
import com.afollestad.nocknock.dialogs.AboutDialog
|
import com.afollestad.nocknock.dialogs.AboutDialog
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||||
import com.afollestad.nocknock.presenters.MainPresenter
|
import com.afollestad.nocknock.ui.addsite.intentToAdd
|
||||||
import com.afollestad.nocknock.presenters.MainView
|
import com.afollestad.nocknock.ui.viewsite.intentToView
|
||||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||||
import com.afollestad.nocknock.utilities.ext.injector
|
import com.afollestad.nocknock.utilities.ext.injector
|
||||||
import com.afollestad.nocknock.utilities.ext.safeRegisterReceiver
|
import com.afollestad.nocknock.utilities.ext.safeRegisterReceiver
|
||||||
|
@ -63,8 +63,10 @@ class MainActivity : AppCompatActivity(), MainView {
|
||||||
@SuppressLint("CommitPrefEdits")
|
@SuppressLint("CommitPrefEdits")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
injector().injectInto(this)
|
injector().injectInto(this)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
presenter.takeView(this)
|
||||||
|
|
||||||
toolbar.inflateMenu(R.menu.menu_main)
|
toolbar.inflateMenu(R.menu.menu_main)
|
||||||
toolbar.setOnMenuItemClickListener { item ->
|
toolbar.setOnMenuItemClickListener { item ->
|
||||||
|
@ -86,8 +88,6 @@ class MainActivity : AppCompatActivity(), MainView {
|
||||||
ADD_SITE_RQ
|
ADD_SITE_RQ
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
presenter.takeView(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -110,15 +110,21 @@ class MainActivity : AppCompatActivity(), MainView {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setModels(models: List<ServerModel>) {
|
override fun setModels(models: List<ServerModel>) {
|
||||||
adapter.set(models)
|
list.post {
|
||||||
emptyText.showOrHide(models.isEmpty())
|
adapter.set(models)
|
||||||
|
emptyText.showOrHide(models.isEmpty())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateModel(model: ServerModel) = adapter.update(model)
|
override fun updateModel(model: ServerModel) {
|
||||||
|
list.post { adapter.update(model) }
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSiteDeleted(model: ServerModel) {
|
override fun onSiteDeleted(model: ServerModel) {
|
||||||
adapter.remove(model)
|
list.post {
|
||||||
emptyText.showOrHide(adapter.itemCount == 0)
|
adapter.remove(model)
|
||||||
|
emptyText.showOrHide(adapter.itemCount == 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun scopeWhileAttached(
|
override fun scopeWhileAttached(
|
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* Designed and developed by Aidan Follestad (@afollestad)
|
* Designed and developed by Aidan Follestad (@afollestad)
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.presenters
|
package com.afollestad.nocknock.ui.main
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.ServerModel
|
||||||
|
@ -58,14 +58,14 @@ class RealMainPresenter @Inject constructor(
|
||||||
|
|
||||||
override fun resume() {
|
override fun resume() {
|
||||||
notificationManager.cancelStatusNotifications()
|
notificationManager.cancelStatusNotifications()
|
||||||
view?.run {
|
view!!.run {
|
||||||
setModels(listOf())
|
setModels(listOf())
|
||||||
scopeWhileAttached(Main) {
|
scopeWhileAttached(Main) {
|
||||||
launch(coroutineContext) {
|
launch(coroutineContext) {
|
||||||
val models = async(IO) {
|
val models = async(IO) {
|
||||||
serverModelStore.get()
|
serverModelStore.get()
|
||||||
}
|
}.await()
|
||||||
setModels(models.await())
|
setModels(models)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ class RealMainPresenter @Inject constructor(
|
||||||
override fun removeSite(site: ServerModel) {
|
override fun removeSite(site: ServerModel) {
|
||||||
checkStatusManager.cancelCheck(site)
|
checkStatusManager.cancelCheck(site)
|
||||||
notificationManager.cancelStatusNotification(site)
|
notificationManager.cancelStatusNotification(site)
|
||||||
view?.scopeWhileAttached(Main) {
|
view!!.scopeWhileAttached(Main) {
|
||||||
launch(coroutineContext) {
|
launch(coroutineContext) {
|
||||||
async(IO) { serverModelStore.delete(site) }.await()
|
async(IO) { serverModelStore.delete(site) }.await()
|
||||||
view?.onSiteDeleted(site)
|
view?.onSiteDeleted(site)
|
||||||
|
@ -95,7 +95,7 @@ class RealMainPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureCheckJobs() {
|
private fun ensureCheckJobs() {
|
||||||
view?.scopeWhileAttached(IO) {
|
view!!.scopeWhileAttached(IO) {
|
||||||
launch(coroutineContext) {
|
launch(coroutineContext) {
|
||||||
checkStatusManager.ensureScheduledChecks()
|
checkStatusManager.ensureScheduledChecks()
|
||||||
}
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* Designed and developed by Aidan Follestad (@afollestad)
|
* Designed and developed by Aidan Follestad (@afollestad)
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.presenters
|
package com.afollestad.nocknock.ui.main
|
||||||
|
|
||||||
import com.afollestad.nocknock.data.ServerModel
|
import com.afollestad.nocknock.data.ServerModel
|
||||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* Designed and developed by Aidan Follestad (@afollestad)
|
* Designed and developed by Aidan Follestad (@afollestad)
|
||||||
*/
|
*/
|
||||||
package com.afollestad.nocknock.ui
|
package com.afollestad.nocknock.ui.viewsite
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
|
@ -39,6 +39,7 @@ import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||||
|
import com.afollestad.nocknock.ui.main.MainActivity
|
||||||
import com.afollestad.nocknock.utilities.ext.formatDate
|
import com.afollestad.nocknock.utilities.ext.formatDate
|
||||||
import com.afollestad.nocknock.utilities.ext.injector
|
import com.afollestad.nocknock.utilities.ext.injector
|
||||||
import com.afollestad.nocknock.utilities.ext.isHttpOrHttps
|
import com.afollestad.nocknock.utilities.ext.isHttpOrHttps
|
||||||
|
@ -108,11 +109,15 @@ class ViewSiteActivity : AppCompatActivity(),
|
||||||
context: Context,
|
context: Context,
|
||||||
intent: Intent
|
intent: Intent
|
||||||
) {
|
) {
|
||||||
log("Received broadcast ${intent.action}")
|
log(
|
||||||
|
"Received broadcast ${intent.action}"
|
||||||
|
)
|
||||||
val model = intent.getSerializableExtra(KEY_VIEW_MODEL) as? ServerModel
|
val model = intent.getSerializableExtra(KEY_VIEW_MODEL) as? ServerModel
|
||||||
if (model != null) {
|
if (model != null) {
|
||||||
this@ViewSiteActivity.currentModel = model
|
this@ViewSiteActivity.currentModel = model
|
||||||
log("Received model update: $currentModel")
|
log(
|
||||||
|
"Received model update: $currentModel"
|
||||||
|
)
|
||||||
displayCurrentModel()
|
displayCurrentModel()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,7 +6,7 @@
|
||||||
android:id="@+id/rootView"
|
android:id="@+id/rootView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".ui.MainActivity"
|
tools:context=".ui.main.MainActivity"
|
||||||
>
|
>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|
|
@ -23,6 +23,9 @@
|
||||||
<string name="please_enter_name">Please enter a name!</string>
|
<string name="please_enter_name">Please enter a name!</string>
|
||||||
<string name="please_enter_url">Please enter a URL.</string>
|
<string name="please_enter_url">Please enter a URL.</string>
|
||||||
<string name="please_enter_valid_url">Please enter a valid URL.</string>
|
<string name="please_enter_valid_url">Please enter a valid URL.</string>
|
||||||
|
<string name="please_enter_check_interval">Please input a check interval.</string>
|
||||||
|
<string name="please_enter_search_term">Please input a search term.</string>
|
||||||
|
<string name="please_enter_javaScript">Please input a validation script.</string>
|
||||||
|
|
||||||
<string name="options">Options</string>
|
<string name="options">Options</string>
|
||||||
<string name="already_checking_sites">Already checking sites!</string>
|
<string name="already_checking_sites">Already checking sites!</string>
|
||||||
|
|
|
@ -0,0 +1,241 @@
|
||||||
|
/*
|
||||||
|
* Licensed under Apache-2.0
|
||||||
|
*
|
||||||
|
* Designed and developed by Aidan Follestad (@afollestad)
|
||||||
|
*/
|
||||||
|
package com.afollestad.nocknock
|
||||||
|
|
||||||
|
import com.afollestad.nocknock.data.ServerModel
|
||||||
|
import com.afollestad.nocknock.data.ValidationMode.JAVASCRIPT
|
||||||
|
import com.afollestad.nocknock.data.ValidationMode.STATUS_CODE
|
||||||
|
import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
||||||
|
import com.afollestad.nocknock.engine.db.ServerModelStore
|
||||||
|
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||||
|
import com.afollestad.nocknock.ui.addsite.AddSiteView
|
||||||
|
import com.afollestad.nocknock.ui.addsite.InputErrors
|
||||||
|
import com.afollestad.nocknock.ui.addsite.RealAddSitePresenter
|
||||||
|
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.nhaarman.mockitokotlin2.any
|
||||||
|
import com.nhaarman.mockitokotlin2.argumentCaptor
|
||||||
|
import com.nhaarman.mockitokotlin2.doAnswer
|
||||||
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
|
import com.nhaarman.mockitokotlin2.never
|
||||||
|
import com.nhaarman.mockitokotlin2.times
|
||||||
|
import com.nhaarman.mockitokotlin2.verify
|
||||||
|
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
|
||||||
|
import com.nhaarman.mockitokotlin2.whenever
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class AddSitePresenterTest {
|
||||||
|
|
||||||
|
private val serverModelStore = mock<ServerModelStore> {
|
||||||
|
on { runBlocking { put(any()) } } doAnswer { inv ->
|
||||||
|
inv.getArgument<ServerModel>(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val checkStatusManager = mock<CheckStatusManager>()
|
||||||
|
private val view = mock<AddSiteView>()
|
||||||
|
|
||||||
|
private val presenter = RealAddSitePresenter(
|
||||||
|
serverModelStore,
|
||||||
|
checkStatusManager
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before fun setup() {
|
||||||
|
doAnswer {
|
||||||
|
val exec = it.getArgument<ScopeReceiver>(1)
|
||||||
|
runBlocking { exec() }
|
||||||
|
Unit
|
||||||
|
}.whenever(view)
|
||||||
|
.scopeWhileAttached(any(), any())
|
||||||
|
|
||||||
|
presenter.takeView(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After fun destroy() {
|
||||||
|
presenter.dropView()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun onUrlInputFocusChange_focused() {
|
||||||
|
presenter.onUrlInputFocusChange(true, "hello")
|
||||||
|
verifyNoMoreInteractions(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun onUrlInputFocusChange_empty() {
|
||||||
|
presenter.onUrlInputFocusChange(false, "")
|
||||||
|
verifyNoMoreInteractions(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun onUrlInputFocusChange_notHttpHttps() {
|
||||||
|
presenter.onUrlInputFocusChange(false, "ftp://hello.com")
|
||||||
|
verify(view).showOrHideUrlSchemeWarning(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun onUrlInputFocusChange_isHttpOrHttps() {
|
||||||
|
presenter.onUrlInputFocusChange(false, "http://hello.com")
|
||||||
|
presenter.onUrlInputFocusChange(false, "https://hello.com")
|
||||||
|
verify(view, times(2)).showOrHideUrlSchemeWarning(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun onValidationModeSelected_statusCode() {
|
||||||
|
presenter.onValidationModeSelected(0)
|
||||||
|
verify(view).showOrHideValidationSearchTerm(false)
|
||||||
|
verify(view).showOrHideScriptInput(false)
|
||||||
|
verify(view).setValidationModeDescription(R.string.validation_mode_status_desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun onValidationModeSelected_termSearch() {
|
||||||
|
presenter.onValidationModeSelected(1)
|
||||||
|
verify(view).showOrHideValidationSearchTerm(true)
|
||||||
|
verify(view).showOrHideScriptInput(false)
|
||||||
|
verify(view).setValidationModeDescription(R.string.validation_mode_term_desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun onValidationModeSelected_javaScript() {
|
||||||
|
presenter.onValidationModeSelected(2)
|
||||||
|
verify(view).showOrHideValidationSearchTerm(false)
|
||||||
|
verify(view).showOrHideScriptInput(true)
|
||||||
|
verify(view).setValidationModeDescription(R.string.validation_mode_javascript_desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalStateException::class)
|
||||||
|
fun onValidationModeSelected_other() {
|
||||||
|
presenter.onValidationModeSelected(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun commit_nameError() {
|
||||||
|
presenter.commit(
|
||||||
|
"",
|
||||||
|
"https://test.com",
|
||||||
|
1,
|
||||||
|
STATUS_CODE,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||||
|
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||||
|
verify(checkStatusManager, never())
|
||||||
|
.scheduleCheck(any(), any(), any(), any())
|
||||||
|
|
||||||
|
val errors = inputErrorsCaptor.firstValue
|
||||||
|
assertThat(errors.name).isEqualTo(R.string.please_enter_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun commit_urlEmptyError() {
|
||||||
|
presenter.commit(
|
||||||
|
"Testing",
|
||||||
|
"",
|
||||||
|
1,
|
||||||
|
STATUS_CODE,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||||
|
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||||
|
verify(checkStatusManager, never())
|
||||||
|
.scheduleCheck(any(), any(), any(), any())
|
||||||
|
|
||||||
|
val errors = inputErrorsCaptor.firstValue
|
||||||
|
assertThat(errors.url).isEqualTo(R.string.please_enter_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun commit_urlFormatError() {
|
||||||
|
presenter.commit(
|
||||||
|
"Testing",
|
||||||
|
"ftp://hello.com",
|
||||||
|
1,
|
||||||
|
STATUS_CODE,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||||
|
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||||
|
verify(checkStatusManager, never())
|
||||||
|
.scheduleCheck(any(), any(), any(), any())
|
||||||
|
|
||||||
|
val errors = inputErrorsCaptor.firstValue
|
||||||
|
assertThat(errors.url).isEqualTo(R.string.please_enter_valid_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun commit_checkIntervalError() {
|
||||||
|
presenter.commit(
|
||||||
|
"Testing",
|
||||||
|
"https://hello.com",
|
||||||
|
-1,
|
||||||
|
STATUS_CODE,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||||
|
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||||
|
verify(checkStatusManager, never())
|
||||||
|
.scheduleCheck(any(), any(), any(), any())
|
||||||
|
|
||||||
|
val errors = inputErrorsCaptor.firstValue
|
||||||
|
assertThat(errors.checkInterval).isEqualTo(R.string.please_enter_check_interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun commit_termSearchError() {
|
||||||
|
presenter.commit(
|
||||||
|
"Testing",
|
||||||
|
"https://hello.com",
|
||||||
|
1,
|
||||||
|
TERM_SEARCH,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||||
|
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||||
|
verify(checkStatusManager, never())
|
||||||
|
.scheduleCheck(any(), any(), any(), any())
|
||||||
|
|
||||||
|
val errors = inputErrorsCaptor.firstValue
|
||||||
|
assertThat(errors.termSearch).isEqualTo(R.string.please_enter_search_term)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun commit_javaScript_error() {
|
||||||
|
presenter.commit(
|
||||||
|
"Testing",
|
||||||
|
"https://hello.com",
|
||||||
|
1,
|
||||||
|
JAVASCRIPT,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
val inputErrorsCaptor = argumentCaptor<InputErrors>()
|
||||||
|
verify(view).setInputErrors(inputErrorsCaptor.capture())
|
||||||
|
verify(checkStatusManager, never())
|
||||||
|
.scheduleCheck(any(), any(), any(), any())
|
||||||
|
|
||||||
|
val errors = inputErrorsCaptor.firstValue
|
||||||
|
assertThat(errors.javaScript).isEqualTo(R.string.please_enter_javaScript)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun commit_success() = runBlocking {
|
||||||
|
presenter.commit(
|
||||||
|
"Testing",
|
||||||
|
"https://hello.com",
|
||||||
|
1,
|
||||||
|
STATUS_CODE,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
val modelCaptor = argumentCaptor<ServerModel>()
|
||||||
|
verify(view).setLoading()
|
||||||
|
verify(serverModelStore).put(modelCaptor.capture())
|
||||||
|
val model = modelCaptor.firstValue
|
||||||
|
verify(view, never()).setInputErrors(any())
|
||||||
|
verify(checkStatusManager).scheduleCheck(
|
||||||
|
site = model,
|
||||||
|
rightNow = true,
|
||||||
|
cancelPrevious = true,
|
||||||
|
fromFinishingJob = false
|
||||||
|
)
|
||||||
|
verify(view).setDoneLoading()
|
||||||
|
verify(view).onSiteAdded()
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,8 +13,8 @@ import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTIO
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
|
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.KEY_UPDATE_MODEL
|
||||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
import com.afollestad.nocknock.engine.statuscheck.CheckStatusManager
|
||||||
import com.afollestad.nocknock.notifications.NockNotificationManager
|
import com.afollestad.nocknock.notifications.NockNotificationManager
|
||||||
import com.afollestad.nocknock.presenters.MainView
|
import com.afollestad.nocknock.ui.main.MainView
|
||||||
import com.afollestad.nocknock.presenters.RealMainPresenter
|
import com.afollestad.nocknock.ui.main.RealMainPresenter
|
||||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import com.nhaarman.mockitokotlin2.any
|
import com.nhaarman.mockitokotlin2.any
|
||||||
|
|
|
@ -78,7 +78,7 @@ class RealNockNotificationManager @Inject constructor(
|
||||||
|
|
||||||
log("Posting status notification for site ${model.id}...")
|
log("Posting status notification for site ${model.id}...")
|
||||||
val viewSiteActivityCls =
|
val viewSiteActivityCls =
|
||||||
Class.forName("com.afollestad.nocknock.ui.ViewSiteActivity")
|
Class.forName("com.afollestad.nocknock.ui.viewsite.ViewSiteActivity")
|
||||||
val openIntent = Intent(app, viewSiteActivityCls).apply {
|
val openIntent = Intent(app, viewSiteActivityCls).apply {
|
||||||
putExtra(KEY_MODEL, model)
|
putExtra(KEY_MODEL, model)
|
||||||
addFlags(FLAG_ACTIVITY_NEW_TASK)
|
addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
|
|
@ -49,6 +49,10 @@ class CheckIntervalLayout(
|
||||||
spinner.adapter = spinnerAdapter
|
spinner.adapter = spinnerAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setError(error: String?) {
|
||||||
|
input.error = error
|
||||||
|
}
|
||||||
|
|
||||||
fun set(interval: Long) {
|
fun set(interval: Long) {
|
||||||
when {
|
when {
|
||||||
interval >= WEEK -> {
|
interval >= WEEK -> {
|
||||||
|
|
|
@ -11,7 +11,9 @@ import android.widget.HorizontalScrollView
|
||||||
import androidx.annotation.CheckResult
|
import androidx.annotation.CheckResult
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
|
import com.afollestad.nocknock.viewcomponents.ext.dimenFloat
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.dimenInt
|
import com.afollestad.nocknock.viewcomponents.ext.dimenInt
|
||||||
|
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
|
||||||
import com.afollestad.nocknock.viewcomponents.ext.trimmedText
|
import com.afollestad.nocknock.viewcomponents.ext.trimmedText
|
||||||
|
import kotlinx.android.synthetic.main.javascript_input_layout.view.error_text
|
||||||
import kotlinx.android.synthetic.main.javascript_input_layout.view.userInput
|
import kotlinx.android.synthetic.main.javascript_input_layout.view.userInput
|
||||||
|
|
||||||
/** @author Aidan Follestad (afollestad) */
|
/** @author Aidan Follestad (afollestad) */
|
||||||
|
@ -33,6 +35,11 @@ class JavaScriptInputLayout(
|
||||||
inflate(context, R.layout.javascript_input_layout, this)
|
inflate(context, R.layout.javascript_input_layout, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setError(error: String?) {
|
||||||
|
error_text.showOrHide(error != null)
|
||||||
|
error_text.text = error
|
||||||
|
}
|
||||||
|
|
||||||
fun setCode(code: String?) {
|
fun setCode(code: String?) {
|
||||||
if (code.isNullOrEmpty()) {
|
if (code.isNullOrEmpty()) {
|
||||||
setDefaultCode()
|
setDefaultCode()
|
||||||
|
@ -41,9 +48,9 @@ class JavaScriptInputLayout(
|
||||||
userInput.setText(code.trim())
|
userInput.setText(code.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setDefaultCode() = userInput.setText(R.string.default_js)
|
|
||||||
|
|
||||||
@CheckResult fun getCode() = userInput.trimmedText()
|
@CheckResult fun getCode() = userInput.trimmedText()
|
||||||
|
|
||||||
fun clear() = userInput.setText("")
|
fun clear() = userInput.setText("")
|
||||||
|
|
||||||
|
private fun setDefaultCode() = userInput.setText(R.string.default_js)
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,17 @@
|
||||||
android:textSize="@dimen/code_font_size"
|
android:textSize="@dimen/code_font_size"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/error_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/content_inset_less"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:text="Please enter some JavaScript!"
|
||||||
|
tools:visibility="visible"
|
||||||
|
style="@style/NockText.Error"
|
||||||
|
/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</merge>
|
</merge>
|
||||||
|
|
|
@ -39,4 +39,10 @@
|
||||||
<item name="android:textColorHint">?android:textColorSecondary</item>
|
<item name="android:textColorHint">?android:textColorSecondary</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="NockText.Error">
|
||||||
|
<item name="android:textSize">@dimen/caption_font_size</item>
|
||||||
|
<item name="android:fontFamily">@font/lato_light</item>
|
||||||
|
<item name="android:textColor">@color/md_red</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue