Bug fixes, UI improvements, fix notifications, etc.

This commit is contained in:
Aidan Follestad 2018-12-01 15:58:24 -08:00
commit 08ddf1ca03
22 changed files with 371 additions and 203 deletions

View file

@ -9,6 +9,8 @@ import android.app.Activity
import android.app.Application import android.app.Application
import android.app.Application.ActivityLifecycleCallbacks import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle 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 typealias ActivityLifeChange = (activity: Activity, resumed: Boolean) -> Unit
@ -36,3 +38,5 @@ fun Application.onActivityLifeChange(cb: ActivityLifeChange) {
) = Unit ) = Unit
}) })
} }
fun String.toHtml() = fromHtml(this, FROM_HTML_MODE_LEGACY)

View file

@ -6,7 +6,9 @@
package com.afollestad.nocknock.di package com.afollestad.nocknock.di
import com.afollestad.nocknock.R 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.AppIconRes
import com.afollestad.nocknock.utilities.qualifiers.MainActivityClass
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import javax.inject.Singleton import javax.inject.Singleton
@ -19,4 +21,9 @@ open class MainModule {
@Singleton @Singleton
@AppIconRes @AppIconRes
fun provideAppIconRes(): Int = R.mipmap.ic_launcher fun provideAppIconRes(): Int = R.mipmap.ic_launcher
@Provides
@Singleton
@MainActivityClass
fun provideMainActivityClass(): Class<*> = MainActivity::class.java
} }

View file

@ -6,12 +6,7 @@
package com.afollestad.nocknock.ui.addsite package com.afollestad.nocknock.ui.addsite
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION
import android.os.Bundle import android.os.Bundle
import android.view.ViewAnimationUtils.createCircularReveal
import android.view.animation.AccelerateInterpolator
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
@ -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.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.ui.main.MainActivity
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.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.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.showOrHide 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.activity_addsite.checkIntervalLayout import kotlinx.android.synthetic.main.activity_addsite.checkIntervalLayout
@ -48,37 +40,20 @@ import kotlin.coroutines.CoroutineContext
import kotlin.math.max import kotlin.math.max
import kotlin.properties.Delegates.notNull import kotlin.properties.Delegates.notNull
private const val KEY_FAB_X = "fab_x" const val KEY_FAB_X = "fab_x"
private const val KEY_FAB_Y = "fab_y" const val KEY_FAB_Y = "fab_y"
private const val KEY_FAB_SIZE = "fab_size" 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)
}
/** @author Aidan Follestad (@afollestad) */ /** @author Aidan Follestad (@afollestad) */
class AddSiteActivity : AppCompatActivity(), AddSiteView { class AddSiteActivity : AppCompatActivity(), AddSiteView {
companion object { var isClosing: Boolean = false
private const val REVEAL_DURATION = 300L var revealCx by notNull<Int>()
} var revealCy by notNull<Int>()
var revealRadius by notNull<Float>()
private var isClosing: Boolean = false
@Inject lateinit var presenter: AddSitePresenter @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") @SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -203,33 +178,6 @@ class AddSiteActivity : AppCompatActivity(), AddSiteView {
exec: ScopeReceiver exec: ScopeReceiver
) = rootView.scopeWhileAttached(context, exec) ) = 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() override fun onBackPressed() = closeActivityWithReveal()
private fun ValidationMode.validationContent() = when (this) { private fun ValidationMode.validationContent() = when (this) {

View file

@ -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()
}
}

View file

@ -5,15 +5,12 @@
*/ */
package com.afollestad.nocknock.ui.main package com.afollestad.nocknock.ui.main
import android.annotation.SuppressLint
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity 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
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
import androidx.recyclerview.widget.LinearLayoutManager 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.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.ui.addsite.intentToAdd
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
@ -43,11 +38,6 @@ import kotlin.coroutines.CoroutineContext
/** @author Aidan Follestad (@afollestad) */ /** @author Aidan Follestad (@afollestad) */
class MainActivity : AppCompatActivity(), MainView { 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() { private val intentReceiver = object : BroadcastReceiver() {
override fun onReceive( override fun onReceive(
context: Context, context: Context,
@ -59,7 +49,6 @@ class MainActivity : AppCompatActivity(), MainView {
private lateinit var adapter: ServerAdapter private lateinit var adapter: ServerAdapter
@SuppressLint("CommitPrefEdits")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -81,12 +70,14 @@ class MainActivity : AppCompatActivity(), MainView {
list.adapter = adapter list.adapter = adapter
list.addItemDecoration(DividerItemDecoration(this, VERTICAL)) list.addItemDecoration(DividerItemDecoration(this, VERTICAL))
fab.setOnClickListener { fab.setOnClickListener { addSite() }
startActivityForResult(
intentToAdd(fab.x, fab.y, fab.measuredWidth), processIntent(intent)
ADD_SITE_RQ }
)
} override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent?.let(::processIntent)
} }
override fun onResume() { override fun onResume() {
@ -145,25 +136,8 @@ class MainActivity : AppCompatActivity(), MainView {
} }
} }
} }
return } else {
} viewSite(model)
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)
} }
} }
} }

View file

@ -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)
}
}

View file

@ -13,14 +13,9 @@ import android.content.IntentFilter
import android.os.Bundle import android.os.Bundle
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.appcompat.app.AppCompatActivity 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.R
import com.afollestad.nocknock.data.LAST_CHECK_NONE import com.afollestad.nocknock.data.LAST_CHECK_NONE
import com.afollestad.nocknock.data.ServerModel 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
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
@ -28,7 +23,6 @@ import com.afollestad.nocknock.data.ValidationMode.TERM_SEARCH
import com.afollestad.nocknock.data.indexToValidationMode import com.afollestad.nocknock.data.indexToValidationMode
import com.afollestad.nocknock.data.textRes import com.afollestad.nocknock.data.textRes
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.ui.main.MainActivity
import com.afollestad.nocknock.utilities.ext.ScopeReceiver import com.afollestad.nocknock.utilities.ext.ScopeReceiver
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
@ -60,12 +54,6 @@ import kotlinx.android.synthetic.main.activity_viewsite.validationModeDescriptio
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.CoroutineContext 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) */ /** @author Aidan Follestad (@afollestad) */
class ViewSiteActivity : AppCompatActivity(), ViewSiteView { class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
@ -88,11 +76,13 @@ class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
toolbar.run { toolbar.run {
setNavigationOnClickListener { finish() } setNavigationOnClickListener { finish() }
inflateMenu(R.menu.menu_viewsite) inflateMenu(R.menu.menu_viewsite)
menu.findItem(R.id.refresh)
.setActionView(R.layout.menu_item_refresh_icon)
.apply {
actionView.setOnClickListener { presenter.checkNow() }
}
setOnMenuItemClickListener { setOnMenuItemClickListener {
when (it.itemId) { maybeRemoveSite()
R.id.refresh -> presenter.checkNow()
R.id.remove -> maybeRemoveSite()
}
return@setOnMenuItemClickListener true return@setOnMenuItemClickListener true
} }
} }
@ -190,7 +180,6 @@ class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
checkIntervalLayout.set(this.checkInterval) checkIntervalLayout.set(this.checkInterval)
responseValidationMode.setSelection(validationMode.value - 1) responseValidationMode.setSelection(validationMode.value - 1)
when (this.validationMode) { when (this.validationMode) {
TERM_SEARCH -> responseValidationSearchTerm.setText(this.validationContent ?: "") TERM_SEARCH -> responseValidationSearchTerm.setText(this.validationContent ?: "")
JAVASCRIPT -> scriptInputLayout.setCode(this.validationContent) JAVASCRIPT -> scriptInputLayout.setCode(this.validationContent)
@ -206,7 +195,7 @@ class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
else R.string.save_changes else R.string.save_changes
) )
invalidateMenuForStatus() invalidateMenuForStatus(model)
} }
override fun setInputErrors(errors: InputErrors) { override fun setInputErrors(errors: InputErrors) {
@ -259,43 +248,6 @@ class ViewSiteActivity : AppCompatActivity(), ViewSiteView {
safeUnregisterReceiver(intentReceiver) 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) { private fun ValidationMode.validationContent() = when (this) {
STATUS_CODE -> null STATUS_CODE -> null
TERM_SEARCH -> responseValidationSearchTerm.trimmedText() TERM_SEARCH -> responseValidationSearchTerm.trimmedText()

View file

@ -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
}
}
}

View file

@ -104,10 +104,7 @@ class RealViewSitePresenter @Inject constructor(
if (intent.action == ACTION_STATUS_UPDATE) { if (intent.action == ACTION_STATUS_UPDATE) {
val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? ServerModel ?: return val model = intent.getSerializableExtra(KEY_UPDATE_MODEL) as? ServerModel ?: return
this.currentModel = model this.currentModel = model
view?.run { view?.displayModel(model)
displayModel(model)
setDoneLoading() // in case this is the result of a manual refresh
}
} }
} }
@ -208,7 +205,6 @@ class RealViewSitePresenter @Inject constructor(
} }
override fun checkNow() = with(view!!) { override fun checkNow() = with(view!!) {
setLoading()
val checkModel = currentModel!!.copy( val checkModel = currentModel!!.copy(
status = WAITING status = WAITING
) )

View 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"
/>

View file

@ -88,7 +88,6 @@ class ViewSitePresenterTest {
presenter.onBroadcast(goodIntent) presenter.onBroadcast(goodIntent)
assertThat(presenter.currentModel()).isEqualTo(model) assertThat(presenter.currentModel()).isEqualTo(model)
verify(view, times(1)).displayModel(model) verify(view, times(1)).displayModel(model)
verify(view).setDoneLoading()
} }
@Test fun onNewIntent() { @Test fun onNewIntent() {
@ -308,7 +307,7 @@ class ViewSitePresenterTest {
) )
presenter.checkNow() presenter.checkNow()
verify(view).setLoading() verify(view, never()).setLoading()
verify(view).displayModel(newModel) verify(view).displayModel(newModel)
verify(checkStatusManager).scheduleCheck( verify(checkStatusManager).scheduleCheck(
site = newModel, site = newModel,

View file

@ -9,7 +9,7 @@ import android.content.ContentValues
import android.database.Cursor import android.database.Cursor
import com.afollestad.nocknock.data.ServerStatus.OK import com.afollestad.nocknock.data.ServerStatus.OK
import com.afollestad.nocknock.utilities.ext.timeString import com.afollestad.nocknock.utilities.ext.timeString
import java.io.Serializable import com.afollestad.nocknock.utilities.providers.IdProvider
import java.lang.System.currentTimeMillis import java.lang.System.currentTimeMillis
import kotlin.math.max import kotlin.math.max
@ -28,7 +28,7 @@ data class ServerModel(
val validationMode: ValidationMode, val validationMode: ValidationMode,
val validationContent: String? = null, val validationContent: String? = null,
val disabled: Boolean = false val disabled: Boolean = false
) : Serializable { ) : IdProvider {
companion object { companion object {
const val TABLE_NAME = "server_models" const val TABLE_NAME = "server_models"
@ -63,6 +63,8 @@ data class ServerModel(
} }
} }
override fun id() = id
fun intervalText(): String { fun intervalText(): String {
val now = currentTimeMillis() val now = currentTimeMillis()
val nextCheck = max(lastCheck, 0) + checkInterval val nextCheck = max(lastCheck, 0) + checkInterval

View file

@ -7,10 +7,12 @@ package com.afollestad.nocknock.engine.db
import android.app.Application import android.app.Application
import android.database.Cursor import android.database.Cursor
import android.util.Log
import com.afollestad.nocknock.data.ServerModel import com.afollestad.nocknock.data.ServerModel
import com.afollestad.nocknock.data.ServerModel.Companion.COLUMN_ID 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.DEFAULT_SORT_ORDER
import com.afollestad.nocknock.data.ServerModel.Companion.TABLE_NAME import com.afollestad.nocknock.data.ServerModel.Companion.TABLE_NAME
import com.afollestad.nocknock.engine.BuildConfig
import com.afollestad.nocknock.utilities.ext.diffFrom import com.afollestad.nocknock.utilities.ext.diffFrom
import javax.inject.Inject import javax.inject.Inject
@ -31,9 +33,22 @@ interface ServerModelStore {
} }
/** @author Aidan Follestad (@afollestad) */ /** @author Aidan Follestad (@afollestad) */
class RealServerModelStore @Inject constructor( class RealServerModelStore @Inject constructor(app: Application) : ServerModelStore {
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) private val dbHelper = ServerModelDbHelper(app)
@ -84,6 +99,9 @@ class RealServerModelStore @Inject constructor(
val newId = writer.insert(TABLE_NAME, null, model.toContentValues()) val newId = writer.insert(TABLE_NAME, null, model.toContentValues())
return model.copy(id = newId.toInt()) return model.copy(id = newId.toInt())
.apply {
log("Inserted new site model: $this")
}
} }
override suspend fun update(model: ServerModel): Int { override suspend fun update(model: ServerModel): Int {
@ -96,9 +114,15 @@ class RealServerModelStore @Inject constructor(
val newValues = model.toContentValues() val newValues = model.toContentValues()
val valuesDiff = oldValues.diffFrom(newValues) 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 selection = "$COLUMN_ID = ?"
val selectionArgs = arrayOf("${model.id}") val selectionArgs = arrayOf("${model.id}")
log("Updated model: $model")
return writer.update(TABLE_NAME, valuesDiff, selection, selectionArgs) return writer.update(TABLE_NAME, valuesDiff, selection, selectionArgs)
} }
@ -109,10 +133,13 @@ class RealServerModelStore @Inject constructor(
val selection = "$COLUMN_ID = ?" val selection = "$COLUMN_ID = ?"
val selectionArgs = arrayOf("$id") val selectionArgs = arrayOf("$id")
log("Deleted model: $id")
return dbHelper.writableDatabase.delete(TABLE_NAME, selection, selectionArgs) return dbHelper.writableDatabase.delete(TABLE_NAME, selection, selectionArgs)
} }
override suspend fun deleteAll(): Int { override suspend fun deleteAll(): Int {
log("Deleted all models")
return dbHelper.writableDatabase.delete(TABLE_NAME, null, null) return dbHelper.writableDatabase.delete(TABLE_NAME, null, null)
} }

View file

@ -93,7 +93,7 @@ class CheckStatusJob : JobService() {
) )
) )
} else { } else {
resultModel updateStatus(site = resultModel)
} }
} }
JAVASCRIPT -> { JAVASCRIPT -> {
@ -108,6 +108,7 @@ class CheckStatusJob : JobService() {
} }
STATUS_CODE -> { STATUS_CODE -> {
// We already know the status code is successful because we are in this else branch // We already know the status code is successful because we are in this else branch
log("Using STATUS_CODE validation, which has passed!")
updateStatus( updateStatus(
resultModel.copy( resultModel.copy(
status = OK, status = OK,

View file

@ -18,10 +18,10 @@ enum class Channel(
val description: Int, val description: Int,
val importance: Int val importance: Int
) { ) {
Statuses( CheckFailures(
id = "statuses", id = "check_failures",
title = R.string.channel_server_status_title, title = R.string.channel_server_check_failures_title,
description = R.string.channel_server_status_description, description = R.string.channel_server_check_failures_description,
importance = IMPORTANCE_DEFAULT importance = IMPORTANCE_DEFAULT
) )
} }

View file

@ -8,16 +8,14 @@ package com.afollestad.nocknock.notifications
import android.annotation.TargetApi import android.annotation.TargetApi
import android.app.Application import android.app.Application
import android.app.NotificationManager 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.os.Build.VERSION_CODES
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat.DEFAULT_VIBRATE import androidx.core.app.NotificationCompat.DEFAULT_VIBRATE
import com.afollestad.nocknock.data.ServerModel 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.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.providers.StringProvider
import com.afollestad.nocknock.utilities.qualifiers.AppIconRes import com.afollestad.nocknock.utilities.qualifiers.AppIconRes
import com.afollestad.nocknock.utilities.util.hasOreo import com.afollestad.nocknock.utilities.util.hasOreo
@ -43,13 +41,11 @@ class RealNockNotificationManager @Inject constructor(
@AppIconRes private val appIconRes: Int, @AppIconRes private val appIconRes: Int,
private val stockManager: NotificationManager, private val stockManager: NotificationManager,
private val bitmapProvider: BitmapProvider, private val bitmapProvider: BitmapProvider,
private val stringProvider: StringProvider private val stringProvider: StringProvider,
private val intentProvider: IntentProvider
) : NockNotificationManager { ) : NockNotificationManager {
companion object { companion object {
private const val BASE_REQUEST_CODE = 44
const val KEY_MODEL = "model"
private fun log(message: String) { private fun log(message: String) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Log.d("NockNotificationManager", message) Log.d("NockNotificationManager", message)
@ -64,10 +60,8 @@ class RealNockNotificationManager @Inject constructor(
log("Is app open? $open") log("Is app open? $open")
} }
override fun createChannels() { override fun createChannels() =
Channel.values() Channel.values().forEach(this::createChannel)
.forEach(this::createChannel)
}
override fun postStatusNotification(model: ServerModel) { override fun postStatusNotification(model: ServerModel) {
if (isAppOpen) { if (isAppOpen) {
@ -77,23 +71,12 @@ class RealNockNotificationManager @Inject constructor(
} }
log("Posting status notification for site ${model.id}...") log("Posting status notification for site ${model.id}...")
val viewSiteActivityCls = val intent = intentProvider.getPendingIntentForViewSite(model)
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 newNotification = notification(app, Statuses) { val newNotification = notification(app, CheckFailures) {
setContentTitle(model.name) setContentTitle(model.name)
setContentText(stringProvider.get(R.string.something_wrong)) setContentText(stringProvider.get(R.string.something_wrong))
setContentIntent(openPendingIntent) setContentIntent(intent)
setSmallIcon(R.drawable.ic_notification) setSmallIcon(R.drawable.ic_notification)
setLargeIcon(bitmapProvider.get(appIconRes)) setLargeIcon(bitmapProvider.get(appIconRes))
setAutoCancel(true) setAutoCancel(true)
@ -109,9 +92,7 @@ class RealNockNotificationManager @Inject constructor(
log("Cancelled status notification for site ${model.id}.") log("Cancelled status notification for site ${model.id}.")
} }
override fun cancelStatusNotifications() { override fun cancelStatusNotifications() = stockManager.cancelAll()
stockManager.cancelAll()
}
@TargetApi(VERSION_CODES.O) @TargetApi(VERSION_CODES.O)
private fun createChannel(channel: Channel) { private fun createChannel(channel: Channel) {
@ -119,10 +100,11 @@ class RealNockNotificationManager @Inject constructor(
log("Not running Android O, channels won't be created.") log("Not running Android O, channels won't be created.")
return return
} }
val notificationChannel = channel.toNotificationChannel(app) val notificationChannel = channel.toNotificationChannel(app)
stockManager.createNotificationChannel(notificationChannel) stockManager.createNotificationChannel(notificationChannel)
log("Created notification channel ${channel.id}") 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
} }

View file

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="channel_server_status_title">Server Statuses</string> <string name="channel_server_check_failures_title">Server Check Failures</string>
<string name="channel_server_status_description"> <string name="channel_server_check_failures_description">
Notifications for server status changes, whether it\'s successful statuses or error statuses. Notifications for Nock Nock status checks failing for your sites. Something has gone
wrong if you see one of these.
</string> </string>
<string name="something_wrong">Something\'s wrong! Tap for details.</string> <string name="something_wrong">Something\'s wrong! Tap for details.</string>

View file

@ -6,7 +6,9 @@
package com.afollestad.nocknock.utilities package com.afollestad.nocknock.utilities
import com.afollestad.nocknock.utilities.providers.BitmapProvider 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.RealBitmapProvider
import com.afollestad.nocknock.utilities.providers.RealIntentProvider
import com.afollestad.nocknock.utilities.providers.RealStringProvider import com.afollestad.nocknock.utilities.providers.RealStringProvider
import com.afollestad.nocknock.utilities.providers.StringProvider import com.afollestad.nocknock.utilities.providers.StringProvider
import dagger.Binds import dagger.Binds
@ -28,4 +30,10 @@ abstract class UtilitiesModule {
abstract fun provideStringProvider( abstract fun provideStringProvider(
stringProvider: RealStringProvider stringProvider: RealStringProvider
): StringProvider ): StringProvider
@Binds
@Singleton
abstract fun provideIntentProvider(
intentProvider: RealIntentProvider
): IntentProvider
} }

View file

@ -7,6 +7,7 @@ package com.afollestad.nocknock.utilities.ext
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.view.View
fun Animator.onEnd(cb: () -> Unit) { fun Animator.onEnd(cb: () -> Unit) {
addListener(object : AnimatorListenerAdapter() { 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()
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -30,7 +30,7 @@
android:background="@null" android:background="@null"
android:fontFamily="@font/fira_mono" android:fontFamily="@font/fira_mono"
android:gravity="top" android:gravity="top"
android:inputType="textMultiLine" android:inputType="textMultiLine|textNoSuggestions"
android:lineSpacingMultiplier="1.6" android:lineSpacingMultiplier="1.6"
android:paddingBottom="@dimen/content_inset_less" android:paddingBottom="@dimen/content_inset_less"
android:paddingEnd="@dimen/content_inset_more" android:paddingEnd="@dimen/content_inset_more"