mirror of
https://github.com/afollestad/nock-nock.git
synced 2025-04-20 03:25:14 +00:00
Bug fixes, UI improvements, fix notifications, etc.
This commit is contained in:
parent
b750957d79
commit
08ddf1ca03
22 changed files with 371 additions and 203 deletions
|
@ -9,6 +9,8 @@ import android.app.Activity
|
|||
import android.app.Application
|
||||
import android.app.Application.ActivityLifecycleCallbacks
|
||||
import android.os.Bundle
|
||||
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||
import androidx.core.text.HtmlCompat.fromHtml
|
||||
|
||||
typealias ActivityLifeChange = (activity: Activity, resumed: Boolean) -> Unit
|
||||
|
||||
|
@ -36,3 +38,5 @@ fun Application.onActivityLifeChange(cb: ActivityLifeChange) {
|
|||
) = Unit
|
||||
})
|
||||
}
|
||||
|
||||
fun String.toHtml() = fromHtml(this, FROM_HTML_MODE_LEGACY)
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
package com.afollestad.nocknock.di
|
||||
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.ui.main.MainActivity
|
||||
import com.afollestad.nocknock.utilities.qualifiers.AppIconRes
|
||||
import com.afollestad.nocknock.utilities.qualifiers.MainActivityClass
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Singleton
|
||||
|
@ -19,4 +21,9 @@ open class MainModule {
|
|||
@Singleton
|
||||
@AppIconRes
|
||||
fun provideAppIconRes(): Int = R.mipmap.ic_launcher
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@MainActivityClass
|
||||
fun provideMainActivityClass(): Class<*> = MainActivity::class.java
|
||||
}
|
||||
|
|
|
@ -6,12 +6,7 @@
|
|||
package com.afollestad.nocknock.ui.addsite
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION
|
||||
import android.os.Bundle
|
||||
import android.view.ViewAnimationUtils.createCircularReveal
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.afollestad.nocknock.R
|
||||
|
@ -20,15 +15,12 @@ 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.data.indexToValidationMode
|
||||
import com.afollestad.nocknock.ui.main.MainActivity
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
import com.afollestad.nocknock.utilities.ext.onEnd
|
||||
import com.afollestad.nocknock.utilities.ext.scopeWhileAttached
|
||||
import com.afollestad.nocknock.viewcomponents.ext.conceal
|
||||
import com.afollestad.nocknock.viewcomponents.ext.onItemSelected
|
||||
import com.afollestad.nocknock.viewcomponents.ext.onLayout
|
||||
import com.afollestad.nocknock.viewcomponents.ext.show
|
||||
import com.afollestad.nocknock.viewcomponents.ext.showOrHide
|
||||
import com.afollestad.nocknock.viewcomponents.ext.trimmedText
|
||||
import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout
|
||||
|
@ -48,37 +40,20 @@ import kotlin.coroutines.CoroutineContext
|
|||
import kotlin.math.max
|
||||
import kotlin.properties.Delegates.notNull
|
||||
|
||||
private const val KEY_FAB_X = "fab_x"
|
||||
private const val KEY_FAB_Y = "fab_y"
|
||||
private const val KEY_FAB_SIZE = "fab_size"
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
fun MainActivity.intentToAdd(
|
||||
x: Float,
|
||||
y: Float,
|
||||
size: Int
|
||||
) = Intent(this, AddSiteActivity::class.java).apply {
|
||||
putExtra(KEY_FAB_X, x)
|
||||
putExtra(KEY_FAB_Y, y)
|
||||
putExtra(KEY_FAB_SIZE, size)
|
||||
addFlags(FLAG_ACTIVITY_NO_ANIMATION)
|
||||
}
|
||||
const val KEY_FAB_X = "fab_x"
|
||||
const val KEY_FAB_Y = "fab_y"
|
||||
const val KEY_FAB_SIZE = "fab_size"
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class AddSiteActivity : AppCompatActivity(), AddSiteView {
|
||||
|
||||
companion object {
|
||||
private const val REVEAL_DURATION = 300L
|
||||
}
|
||||
|
||||
private var isClosing: Boolean = false
|
||||
var isClosing: Boolean = false
|
||||
var revealCx by notNull<Int>()
|
||||
var revealCy by notNull<Int>()
|
||||
var revealRadius by notNull<Float>()
|
||||
|
||||
@Inject lateinit var presenter: AddSitePresenter
|
||||
|
||||
private var revealCx by notNull<Int>()
|
||||
private var revealCy by notNull<Int>()
|
||||
private var revealRadius by notNull<Float>()
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -203,33 +178,6 @@ class AddSiteActivity : AppCompatActivity(), AddSiteView {
|
|||
exec: ScopeReceiver
|
||||
) = rootView.scopeWhileAttached(context, exec)
|
||||
|
||||
private fun circularRevealActivity() {
|
||||
val circularReveal =
|
||||
createCircularReveal(rootView, revealCx, revealCy, 0f, revealRadius)
|
||||
.apply {
|
||||
duration = REVEAL_DURATION
|
||||
interpolator = DecelerateInterpolator()
|
||||
}
|
||||
rootView.show()
|
||||
circularReveal.start()
|
||||
}
|
||||
|
||||
private fun closeActivityWithReveal() {
|
||||
if (isClosing) return
|
||||
isClosing = true
|
||||
createCircularReveal(rootView, revealCx, revealCy, revealRadius, 0f)
|
||||
.apply {
|
||||
duration = REVEAL_DURATION
|
||||
interpolator = AccelerateInterpolator()
|
||||
onEnd {
|
||||
rootView.conceal()
|
||||
finish()
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() = closeActivityWithReveal()
|
||||
|
||||
private fun ValidationMode.validationContent() = when (this) {
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.addsite
|
||||
|
||||
import android.view.ViewAnimationUtils
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import com.afollestad.nocknock.utilities.ext.onEnd
|
||||
import com.afollestad.nocknock.viewcomponents.ext.conceal
|
||||
import com.afollestad.nocknock.viewcomponents.ext.show
|
||||
import kotlinx.android.synthetic.main.activity_addsite.rootView
|
||||
|
||||
const val REVEAL_DURATION = 300L
|
||||
|
||||
internal fun AddSiteActivity.circularRevealActivity() {
|
||||
val circularReveal =
|
||||
ViewAnimationUtils.createCircularReveal(rootView, revealCx, revealCy, 0f, revealRadius)
|
||||
.apply {
|
||||
duration = REVEAL_DURATION
|
||||
interpolator = DecelerateInterpolator()
|
||||
}
|
||||
rootView.show()
|
||||
circularReveal.start()
|
||||
}
|
||||
|
||||
internal fun AddSiteActivity.closeActivityWithReveal() {
|
||||
if (isClosing) return
|
||||
isClosing = true
|
||||
ViewAnimationUtils.createCircularReveal(rootView, revealCx, revealCy, revealRadius, 0f)
|
||||
.apply {
|
||||
duration = REVEAL_DURATION
|
||||
interpolator = AccelerateInterpolator()
|
||||
onEnd {
|
||||
rootView.conceal()
|
||||
finish()
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
start()
|
||||
}
|
||||
}
|
|
@ -5,15 +5,12 @@
|
|||
*/
|
||||
package com.afollestad.nocknock.ui.main
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||
import androidx.core.text.HtmlCompat.fromHtml
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -24,8 +21,6 @@ import com.afollestad.nocknock.adapter.ServerAdapter
|
|||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.dialogs.AboutDialog
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.ui.addsite.intentToAdd
|
||||
import com.afollestad.nocknock.ui.viewsite.intentToView
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
import com.afollestad.nocknock.utilities.ext.safeRegisterReceiver
|
||||
|
@ -43,11 +38,6 @@ import kotlin.coroutines.CoroutineContext
|
|||
/** @author Aidan Follestad (@afollestad) */
|
||||
class MainActivity : AppCompatActivity(), MainView {
|
||||
|
||||
companion object {
|
||||
private const val ADD_SITE_RQ = 6969
|
||||
private const val VIEW_SITE_RQ = 6923
|
||||
}
|
||||
|
||||
private val intentReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
|
@ -59,7 +49,6 @@ class MainActivity : AppCompatActivity(), MainView {
|
|||
|
||||
private lateinit var adapter: ServerAdapter
|
||||
|
||||
@SuppressLint("CommitPrefEdits")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
@ -81,12 +70,14 @@ class MainActivity : AppCompatActivity(), MainView {
|
|||
list.adapter = adapter
|
||||
list.addItemDecoration(DividerItemDecoration(this, VERTICAL))
|
||||
|
||||
fab.setOnClickListener {
|
||||
startActivityForResult(
|
||||
intentToAdd(fab.x, fab.y, fab.measuredWidth),
|
||||
ADD_SITE_RQ
|
||||
)
|
||||
}
|
||||
fab.setOnClickListener { addSite() }
|
||||
|
||||
processIntent(intent)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
intent?.let(::processIntent)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -145,25 +136,8 @@ class MainActivity : AppCompatActivity(), MainView {
|
|||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
startActivityForResult(intentToView(model), VIEW_SITE_RQ)
|
||||
}
|
||||
|
||||
private fun maybeRemoveSite(model: ServerModel) {
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.remove_site)
|
||||
message(
|
||||
text = fromHtml(
|
||||
context.getString(R.string.remove_site_prompt, model.name),
|
||||
FROM_HTML_MODE_LEGACY
|
||||
)
|
||||
)
|
||||
positiveButton(R.string.remove) {
|
||||
presenter.removeSite(model)
|
||||
}
|
||||
negativeButton(android.R.string.cancel)
|
||||
} else {
|
||||
viewSite(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.main
|
||||
|
||||
import android.content.Intent
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.toHtml
|
||||
import com.afollestad.nocknock.ui.addsite.AddSiteActivity
|
||||
import com.afollestad.nocknock.ui.addsite.KEY_FAB_SIZE
|
||||
import com.afollestad.nocknock.ui.addsite.KEY_FAB_X
|
||||
import com.afollestad.nocknock.ui.addsite.KEY_FAB_Y
|
||||
import com.afollestad.nocknock.ui.viewsite.KEY_VIEW_MODEL
|
||||
import com.afollestad.nocknock.ui.viewsite.ViewSiteActivity
|
||||
import com.afollestad.nocknock.utilities.providers.RealIntentProvider.Companion.KEY_VIEW_NOTIFICATION_MODEL
|
||||
import kotlinx.android.synthetic.main.activity_main.fab
|
||||
|
||||
internal const val VIEW_SITE_RQ = 6923
|
||||
internal const val ADD_SITE_RQ = 6969
|
||||
|
||||
internal fun MainActivity.addSite() {
|
||||
startActivityForResult(intentToAdd(fab.x, fab.y, fab.measuredWidth), ADD_SITE_RQ)
|
||||
}
|
||||
|
||||
private fun MainActivity.intentToAdd(
|
||||
x: Float,
|
||||
y: Float,
|
||||
size: Int
|
||||
) = Intent(this, AddSiteActivity::class.java).apply {
|
||||
putExtra(KEY_FAB_X, x)
|
||||
putExtra(KEY_FAB_Y, y)
|
||||
putExtra(KEY_FAB_SIZE, size)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||
}
|
||||
|
||||
internal fun MainActivity.viewSite(model: ServerModel) {
|
||||
startActivityForResult(intentToView(model), VIEW_SITE_RQ)
|
||||
}
|
||||
|
||||
private fun MainActivity.intentToView(model: ServerModel) =
|
||||
Intent(this, ViewSiteActivity::class.java).apply {
|
||||
putExtra(KEY_VIEW_MODEL, model)
|
||||
}
|
||||
|
||||
internal fun MainActivity.maybeRemoveSite(model: ServerModel) {
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.remove_site)
|
||||
message(text = context.getString(R.string.remove_site_prompt, model.name).toHtml())
|
||||
positiveButton(R.string.remove) { presenter.removeSite(model) }
|
||||
negativeButton(android.R.string.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun MainActivity.processIntent(intent: Intent) {
|
||||
if (intent.hasExtra(KEY_VIEW_NOTIFICATION_MODEL)) {
|
||||
val model = intent.getSerializableExtra(KEY_VIEW_NOTIFICATION_MODEL) as ServerModel
|
||||
viewSite(model)
|
||||
}
|
||||
}
|
|
@ -13,14 +13,9 @@ import android.content.IntentFilter
|
|||
import android.os.Bundle
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.LAST_CHECK_NONE
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerStatus.CHECKING
|
||||
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.STATUS_CODE
|
||||
|
@ -28,7 +23,6 @@ import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
|
|||
import com.afollestad.nocknock.data.indexToValidationMode
|
||||
import com.afollestad.nocknock.data.textRes
|
||||
import com.afollestad.nocknock.engine.statuscheck.CheckStatusJob.Companion.ACTION_STATUS_UPDATE
|
||||
import com.afollestad.nocknock.ui.main.MainActivity
|
||||
import com.afollestad.nocknock.utilities.ext.ScopeReceiver
|
||||
import com.afollestad.nocknock.utilities.ext.formatDate
|
||||
import com.afollestad.nocknock.utilities.ext.injector
|
||||
|
@ -60,12 +54,6 @@ import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescriptio
|
|||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
fun MainActivity.intentToView(model: ServerModel) =
|
||||
Intent(this, ViewSiteActivity::class.java).apply {
|
||||
putExtra(KEY_VIEW_MODEL, model)
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
|
||||
|
||||
|
@ -88,11 +76,13 @@ class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
|
|||
toolbar.run {
|
||||
setNavigationOnClickListener { finish() }
|
||||
inflateMenu(R.menu.menu_viewsite)
|
||||
menu.findItem(R.id.refresh)
|
||||
.setActionView(R.layout.menu_item_refresh_icon)
|
||||
.apply {
|
||||
actionView.setOnClickListener { presenter.checkNow() }
|
||||
}
|
||||
setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.refresh -> presenter.checkNow()
|
||||
R.id.remove -> maybeRemoveSite()
|
||||
}
|
||||
maybeRemoveSite()
|
||||
return@setOnMenuItemClickListener true
|
||||
}
|
||||
}
|
||||
|
@ -190,7 +180,6 @@ class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
|
|||
checkIntervalLayout.set(this.checkInterval)
|
||||
|
||||
responseValidationMode.setSelection(validationMode.value - 1)
|
||||
|
||||
when (this.validationMode) {
|
||||
TERM_SEARCH -> responseValidationSearchTerm.setText(this.validationContent ?: "")
|
||||
JAVASCRIPT -> scriptInputLayout.setCode(this.validationContent)
|
||||
|
@ -206,7 +195,7 @@ class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
|
|||
else R.string.save_changes
|
||||
)
|
||||
|
||||
invalidateMenuForStatus()
|
||||
invalidateMenuForStatus(model)
|
||||
}
|
||||
|
||||
override fun setInputErrors(errors: InputErrors) {
|
||||
|
@ -259,43 +248,6 @@ class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
|
|||
safeUnregisterReceiver(intentReceiver)
|
||||
}
|
||||
|
||||
private fun maybeRemoveSite() {
|
||||
val model = presenter.currentModel()
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.remove_site)
|
||||
message(
|
||||
text = HtmlCompat.fromHtml(
|
||||
context.getString(R.string.remove_site_prompt, model.name),
|
||||
FROM_HTML_MODE_LEGACY
|
||||
)
|
||||
)
|
||||
positiveButton(R.string.remove) { presenter.removeSite() }
|
||||
negativeButton(android.R.string.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeDisableChecks() {
|
||||
val model = presenter.currentModel()
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.disable_automatic_checks)
|
||||
message(
|
||||
text = HtmlCompat.fromHtml(
|
||||
context.getString(R.string.disable_automatic_checks_prompt, model.name),
|
||||
FROM_HTML_MODE_LEGACY
|
||||
)
|
||||
)
|
||||
positiveButton(R.string.disable) { presenter.disableChecks() }
|
||||
negativeButton(android.R.string.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun invalidateMenuForStatus() {
|
||||
val model = presenter.currentModel()
|
||||
val item = toolbar.menu.findItem(R.id.refresh)
|
||||
item.isEnabled = model.status != CHECKING &&
|
||||
model.status != WAITING
|
||||
}
|
||||
|
||||
private fun ValidationMode.validationContent() = when (this) {
|
||||
STATUS_CODE -> null
|
||||
TERM_SEARCH -> responseValidationSearchTerm.trimmedText()
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.ui.viewsite
|
||||
|
||||
import android.widget.ImageView
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.nocknock.R
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.isPending
|
||||
import com.afollestad.nocknock.toHtml
|
||||
import com.afollestad.nocknock.utilities.ext.animateRotation
|
||||
import kotlinx.android.synthetic.main.activity_viewsite.toolbar
|
||||
|
||||
internal fun ViewSiteActivity.maybeRemoveSite() {
|
||||
val model = presenter.currentModel()
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.remove_site)
|
||||
message(text = context.getString(R.string.remove_site_prompt, model.name).toHtml())
|
||||
positiveButton(R.string.remove) { presenter.removeSite() }
|
||||
negativeButton(android.R.string.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ViewSiteActivity.maybeDisableChecks() {
|
||||
val model = presenter.currentModel()
|
||||
MaterialDialog(this).show {
|
||||
title(R.string.disable_automatic_checks)
|
||||
message(
|
||||
text = context.getString(R.string.disable_automatic_checks_prompt, model.name).toHtml()
|
||||
)
|
||||
positiveButton(R.string.disable) { presenter.disableChecks() }
|
||||
negativeButton(android.R.string.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ViewSiteActivity.invalidateMenuForStatus(model: ServerModel) {
|
||||
val refreshIcon = toolbar.menu.findItem(R.id.refresh)
|
||||
.actionView as ImageView
|
||||
|
||||
if (model.status.isPending()) {
|
||||
refreshIcon.animateRotation()
|
||||
} else {
|
||||
refreshIcon.run {
|
||||
animate().cancel()
|
||||
rotation = 0f
|
||||
}
|
||||
}
|
||||
}
|
|
@ -104,10 +104,7 @@ class RealViewSitePresenter @Inject constructor(
|
|||
if (intent.action == ACTION_STATUS_UPDATE) {
|
||||
val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? ServerModel ?: return
|
||||
this.currentModel = model
|
||||
view?.run {
|
||||
displayModel(model)
|
||||
setDoneLoading() // in case this is the result of a manual refresh
|
||||
}
|
||||
view?.displayModel(model)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -208,7 +205,6 @@ class RealViewSitePresenter @Inject constructor(
|
|||
}
|
||||
|
||||
override fun checkNow() = with(view!!) {
|
||||
setLoading()
|
||||
val checkModel = currentModel!!.copy(
|
||||
status = WAITING
|
||||
)
|
||||
|
|
9
app/src/main/res/layout/menu_item_refresh_icon.xml
Normal file
9
app/src/main/res/layout/menu_item_refresh_icon.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ImageView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/refresh_status"
|
||||
android:src="@drawable/ic_action_refresh"
|
||||
style="@android:style/Widget.ActionButton"
|
||||
/>
|
|
@ -88,7 +88,6 @@ class ViewSitePresenterTest {
|
|||
presenter.onBroadcast(goodIntent)
|
||||
assertThat(presenter.currentModel()).isEqualTo(model)
|
||||
verify(view, times(1)).displayModel(model)
|
||||
verify(view).setDoneLoading()
|
||||
}
|
||||
|
||||
@Test fun onNewIntent() {
|
||||
|
@ -308,7 +307,7 @@ class ViewSitePresenterTest {
|
|||
)
|
||||
presenter.checkNow()
|
||||
|
||||
verify(view).setLoading()
|
||||
verify(view, never()).setLoading()
|
||||
verify(view).displayModel(newModel)
|
||||
verify(checkStatusManager).scheduleCheck(
|
||||
site = newModel,
|
||||
|
|
|
@ -9,7 +9,7 @@ import android.content.ContentValues
|
|||
import android.database.Cursor
|
||||
import com.afollestad.nocknock.data.ServerStatus.OK
|
||||
import com.afollestad.nocknock.utilities.ext.timeString
|
||||
import java.io.Serializable
|
||||
import com.afollestad.nocknock.utilities.providers.IdProvider
|
||||
import java.lang.System.currentTimeMillis
|
||||
import kotlin.math.max
|
||||
|
||||
|
@ -28,7 +28,7 @@ data class ServerModel(
|
|||
val validationMode: ValidationMode,
|
||||
val validationContent: String? = null,
|
||||
val disabled: Boolean = false
|
||||
) : Serializable {
|
||||
) : IdProvider {
|
||||
|
||||
companion object {
|
||||
const val TABLE_NAME = "server_models"
|
||||
|
@ -63,6 +63,8 @@ data class ServerModel(
|
|||
}
|
||||
}
|
||||
|
||||
override fun id() = id
|
||||
|
||||
fun intervalText(): String {
|
||||
val now = currentTimeMillis()
|
||||
val nextCheck = max(lastCheck, 0) + checkInterval
|
||||
|
|
|
@ -7,10 +7,12 @@ package com.afollestad.nocknock.engine.db
|
|||
|
||||
import android.app.Application
|
||||
import android.database.Cursor
|
||||
import android.util.Log
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.data.ServerModel.Companion.COLUMN_ID
|
||||
import com.afollestad.nocknock.data.ServerModel.Companion.DEFAULT_SORT_ORDER
|
||||
import com.afollestad.nocknock.data.ServerModel.Companion.TABLE_NAME
|
||||
import com.afollestad.nocknock.engine.BuildConfig
|
||||
import com.afollestad.nocknock.utilities.ext.diffFrom
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -31,9 +33,22 @@ interface ServerModelStore {
|
|||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class RealServerModelStore @Inject constructor(
|
||||
app: Application
|
||||
) : ServerModelStore {
|
||||
class RealServerModelStore @Inject constructor(app: Application) : ServerModelStore {
|
||||
|
||||
companion object {
|
||||
private fun log(
|
||||
message: String,
|
||||
warning: Boolean = false
|
||||
) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
if (warning) {
|
||||
Log.w("ServerModelStore", message)
|
||||
} else {
|
||||
Log.d("ServerModelStore", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val dbHelper = ServerModelDbHelper(app)
|
||||
|
||||
|
@ -84,6 +99,9 @@ class RealServerModelStore @Inject constructor(
|
|||
val newId = writer.insert(TABLE_NAME, null, model.toContentValues())
|
||||
|
||||
return model.copy(id = newId.toInt())
|
||||
.apply {
|
||||
log("Inserted new site model: $this")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun update(model: ServerModel): Int {
|
||||
|
@ -96,9 +114,15 @@ class RealServerModelStore @Inject constructor(
|
|||
val newValues = model.toContentValues()
|
||||
val valuesDiff = oldValues.diffFrom(newValues)
|
||||
|
||||
if (valuesDiff.size() == 0) {
|
||||
log("Nothing has changed - nothing to update!", warning = true)
|
||||
return 0
|
||||
}
|
||||
|
||||
val selection = "$COLUMN_ID = ?"
|
||||
val selectionArgs = arrayOf("${model.id}")
|
||||
|
||||
log("Updated model: $model")
|
||||
return writer.update(TABLE_NAME, valuesDiff, selection, selectionArgs)
|
||||
}
|
||||
|
||||
|
@ -109,10 +133,13 @@ class RealServerModelStore @Inject constructor(
|
|||
|
||||
val selection = "$COLUMN_ID = ?"
|
||||
val selectionArgs = arrayOf("$id")
|
||||
|
||||
log("Deleted model: $id")
|
||||
return dbHelper.writableDatabase.delete(TABLE_NAME, selection, selectionArgs)
|
||||
}
|
||||
|
||||
override suspend fun deleteAll(): Int {
|
||||
log("Deleted all models")
|
||||
return dbHelper.writableDatabase.delete(TABLE_NAME, null, null)
|
||||
}
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ class CheckStatusJob : JobService() {
|
|||
)
|
||||
)
|
||||
} else {
|
||||
resultModel
|
||||
updateStatus(site = resultModel)
|
||||
}
|
||||
}
|
||||
JAVASCRIPT -> {
|
||||
|
@ -108,6 +108,7 @@ class CheckStatusJob : JobService() {
|
|||
}
|
||||
STATUS_CODE -> {
|
||||
// We already know the status code is successful because we are in this else branch
|
||||
log("Using STATUS_CODE validation, which has passed!")
|
||||
updateStatus(
|
||||
resultModel.copy(
|
||||
status = OK,
|
||||
|
|
|
@ -18,10 +18,10 @@ enum class Channel(
|
|||
val description: Int,
|
||||
val importance: Int
|
||||
) {
|
||||
Statuses(
|
||||
id = "statuses",
|
||||
title = R.string.channel_server_status_title,
|
||||
description = R.string.channel_server_status_description,
|
||||
CheckFailures(
|
||||
id = "check_failures",
|
||||
title = R.string.channel_server_check_failures_title,
|
||||
description = R.string.channel_server_check_failures_description,
|
||||
importance = IMPORTANCE_DEFAULT
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,16 +8,14 @@ package com.afollestad.nocknock.notifications
|
|||
import android.annotation.TargetApi
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_CANCEL_CURRENT
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat.DEFAULT_VIBRATE
|
||||
import com.afollestad.nocknock.data.ServerModel
|
||||
import com.afollestad.nocknock.notifications.Channel.Statuses
|
||||
import com.afollestad.nocknock.notifications.Channel.CheckFailures
|
||||
import com.afollestad.nocknock.utilities.providers.BitmapProvider
|
||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||
import com.afollestad.nocknock.utilities.providers.RealIntentProvider.Companion.BASE_NOTIFICATION_REQUEST_CODE
|
||||
import com.afollestad.nocknock.utilities.providers.StringProvider
|
||||
import com.afollestad.nocknock.utilities.qualifiers.AppIconRes
|
||||
import com.afollestad.nocknock.utilities.util.hasOreo
|
||||
|
@ -43,13 +41,11 @@ class RealNockNotificationManager @Inject constructor(
|
|||
@AppIconRes private val appIconRes: Int,
|
||||
private val stockManager: NotificationManager,
|
||||
private val bitmapProvider: BitmapProvider,
|
||||
private val stringProvider: StringProvider
|
||||
private val stringProvider: StringProvider,
|
||||
private val intentProvider: IntentProvider
|
||||
) : NockNotificationManager {
|
||||
|
||||
companion object {
|
||||
private const val BASE_REQUEST_CODE = 44
|
||||
|
||||
const val KEY_MODEL = "model"
|
||||
|
||||
private fun log(message: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d("NockNotificationManager", message)
|
||||
|
@ -64,10 +60,8 @@ class RealNockNotificationManager @Inject constructor(
|
|||
log("Is app open? $open")
|
||||
}
|
||||
|
||||
override fun createChannels() {
|
||||
Channel.values()
|
||||
.forEach(this::createChannel)
|
||||
}
|
||||
override fun createChannels() =
|
||||
Channel.values().forEach(this::createChannel)
|
||||
|
||||
override fun postStatusNotification(model: ServerModel) {
|
||||
if (isAppOpen) {
|
||||
|
@ -77,23 +71,12 @@ class RealNockNotificationManager @Inject constructor(
|
|||
}
|
||||
|
||||
log("Posting status notification for site ${model.id}...")
|
||||
val viewSiteActivityCls =
|
||||
Class.forName("com.afollestad.nocknock.ui.viewsite.ViewSiteActivity")
|
||||
val openIntent = Intent(app, viewSiteActivityCls).apply {
|
||||
putExtra(KEY_MODEL, model)
|
||||
addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
val openPendingIntent = PendingIntent.getBroadcast(
|
||||
app,
|
||||
BASE_REQUEST_CODE + model.id,
|
||||
openIntent,
|
||||
FLAG_CANCEL_CURRENT
|
||||
)
|
||||
val intent = intentProvider.getPendingIntentForViewSite(model)
|
||||
|
||||
val newNotification = notification(app, Statuses) {
|
||||
val newNotification = notification(app, CheckFailures) {
|
||||
setContentTitle(model.name)
|
||||
setContentText(stringProvider.get(R.string.something_wrong))
|
||||
setContentIntent(openPendingIntent)
|
||||
setContentIntent(intent)
|
||||
setSmallIcon(R.drawable.ic_notification)
|
||||
setLargeIcon(bitmapProvider.get(appIconRes))
|
||||
setAutoCancel(true)
|
||||
|
@ -109,9 +92,7 @@ class RealNockNotificationManager @Inject constructor(
|
|||
log("Cancelled status notification for site ${model.id}.")
|
||||
}
|
||||
|
||||
override fun cancelStatusNotifications() {
|
||||
stockManager.cancelAll()
|
||||
}
|
||||
override fun cancelStatusNotifications() = stockManager.cancelAll()
|
||||
|
||||
@TargetApi(VERSION_CODES.O)
|
||||
private fun createChannel(channel: Channel) {
|
||||
|
@ -119,10 +100,11 @@ class RealNockNotificationManager @Inject constructor(
|
|||
log("Not running Android O, channels won't be created.")
|
||||
return
|
||||
}
|
||||
|
||||
val notificationChannel = channel.toNotificationChannel(app)
|
||||
stockManager.createNotificationChannel(notificationChannel)
|
||||
log("Created notification channel ${channel.id}")
|
||||
}
|
||||
|
||||
private fun ServerModel.notificationId() = BASE_REQUEST_CODE + this.id
|
||||
private fun ServerModel.notificationId() = BASE_NOTIFICATION_REQUEST_CODE + this.id
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="channel_server_status_title">Server Statuses</string>
|
||||
<string name="channel_server_status_description">
|
||||
Notifications for server status changes, whether it\'s successful statuses or error statuses.
|
||||
<string name="channel_server_check_failures_title">Server Check Failures</string>
|
||||
<string name="channel_server_check_failures_description">
|
||||
Notifications for Nock Nock status checks failing for your sites. Something has gone
|
||||
wrong if you see one of these.
|
||||
</string>
|
||||
|
||||
<string name="something_wrong">Something\'s wrong! Tap for details.</string>
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
package com.afollestad.nocknock.utilities
|
||||
|
||||
import com.afollestad.nocknock.utilities.providers.BitmapProvider
|
||||
import com.afollestad.nocknock.utilities.providers.IntentProvider
|
||||
import com.afollestad.nocknock.utilities.providers.RealBitmapProvider
|
||||
import com.afollestad.nocknock.utilities.providers.RealIntentProvider
|
||||
import com.afollestad.nocknock.utilities.providers.RealStringProvider
|
||||
import com.afollestad.nocknock.utilities.providers.StringProvider
|
||||
import dagger.Binds
|
||||
|
@ -28,4 +30,10 @@ abstract class UtilitiesModule {
|
|||
abstract fun provideStringProvider(
|
||||
stringProvider: RealStringProvider
|
||||
): StringProvider
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun provideIntentProvider(
|
||||
intentProvider: RealIntentProvider
|
||||
): IntentProvider
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ package com.afollestad.nocknock.utilities.ext
|
|||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.view.View
|
||||
|
||||
fun Animator.onEnd(cb: () -> Unit) {
|
||||
addListener(object : AnimatorListenerAdapter() {
|
||||
|
@ -16,3 +17,34 @@ fun Animator.onEnd(cb: () -> Unit) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun View.animateRotation(
|
||||
loop: Boolean = true,
|
||||
firstPass: Boolean = true,
|
||||
durationPerRotation: Long = 1000,
|
||||
degreesPerRotation: Float = 360f
|
||||
) {
|
||||
if (firstPass) {
|
||||
animate().cancel()
|
||||
}
|
||||
animate()
|
||||
.rotationBy(degreesPerRotation)
|
||||
.setDuration(durationPerRotation)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
|
||||
var isCancelled = false
|
||||
|
||||
override fun onAnimationCancel(animation: Animator?) {
|
||||
super.onAnimationCancel(animation)
|
||||
isCancelled = true
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
super.onAnimationEnd(animation)
|
||||
if (loop && !isCancelled) {
|
||||
animateRotation(loop = true, firstPass = false)
|
||||
}
|
||||
}
|
||||
})
|
||||
.start()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.utilities.providers
|
||||
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_CANCEL_CURRENT
|
||||
import android.content.Intent
|
||||
import com.afollestad.nocknock.utilities.qualifiers.MainActivityClass
|
||||
import java.io.Serializable
|
||||
import javax.inject.Inject
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
interface IdProvider : Serializable {
|
||||
|
||||
fun id(): Int
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
interface IntentProvider {
|
||||
|
||||
fun getPendingIntentForViewSite(
|
||||
model: IdProvider
|
||||
): PendingIntent
|
||||
}
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
class RealIntentProvider @Inject constructor(
|
||||
private val app: Application,
|
||||
@MainActivityClass private val mainActivity: Class<*>
|
||||
) : IntentProvider {
|
||||
|
||||
companion object {
|
||||
const val BASE_NOTIFICATION_REQUEST_CODE = 40
|
||||
const val KEY_VIEW_NOTIFICATION_MODEL = "model"
|
||||
}
|
||||
|
||||
override fun getPendingIntentForViewSite(model: IdProvider): PendingIntent {
|
||||
val openIntent = getIntentForViewSite(model)
|
||||
return PendingIntent.getActivity(
|
||||
app,
|
||||
BASE_NOTIFICATION_REQUEST_CODE + model.id(),
|
||||
openIntent,
|
||||
FLAG_CANCEL_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
private fun getIntentForViewSite(model: IdProvider) =
|
||||
Intent(app, mainActivity).apply {
|
||||
putExtra(KEY_VIEW_NOTIFICATION_MODEL, model)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Licensed under Apache-2.0
|
||||
*
|
||||
* Designed and developed by Aidan Follestad (@afollestad)
|
||||
*/
|
||||
package com.afollestad.nocknock.utilities.qualifiers
|
||||
|
||||
import javax.inject.Qualifier
|
||||
import kotlin.annotation.AnnotationRetention.RUNTIME
|
||||
|
||||
/** @author Aidan Follestad (@afollestad) */
|
||||
@Qualifier
|
||||
@Retention(RUNTIME)
|
||||
annotation class MainActivityClass
|
|
@ -30,7 +30,7 @@
|
|||
android:background="@null"
|
||||
android:fontFamily="@font/fira_mono"
|
||||
android:gravity="top"
|
||||
android:inputType="textMultiLine"
|
||||
android:inputType="textMultiLine|textNoSuggestions"
|
||||
android:lineSpacingMultiplier="1.6"
|
||||
android:paddingBottom="@dimen/content_inset_less"
|
||||
android:paddingEnd="@dimen/content_inset_more"
|
||||
|
|
Loading…
Add table
Reference in a new issue