diff --git a/app/src/main/java/com/afollestad/nocknock/AppExt.kt b/app/src/main/java/com/afollestad/nocknock/AppExt.kt index a1c7bc9..efd36d4 100644 --- a/app/src/main/java/com/afollestad/nocknock/AppExt.kt +++ b/app/src/main/java/com/afollestad/nocknock/AppExt.kt @@ -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) diff --git a/app/src/main/java/com/afollestad/nocknock/di/MainModule.kt b/app/src/main/java/com/afollestad/nocknock/di/MainModule.kt index afe34b8..6e873ca 100644 --- a/app/src/main/java/com/afollestad/nocknock/di/MainModule.kt +++ b/app/src/main/java/com/afollestad/nocknock/di/MainModule.kt @@ -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 } diff --git a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt b/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt index d4e092e..65e71c8 100644 --- a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt +++ b/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivity.kt @@ -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() + var revealCy by notNull() + var revealRadius by notNull() @Inject lateinit var presenter: AddSitePresenter - private var revealCx by notNull() - private var revealCy by notNull() - private var revealRadius by notNull() - @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) { diff --git a/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivityExt.kt b/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivityExt.kt new file mode 100644 index 0000000..7f5eb7f --- /dev/null +++ b/app/src/main/java/com/afollestad/nocknock/ui/addsite/AddSiteActivityExt.kt @@ -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() + } +} diff --git a/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivity.kt b/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivity.kt index e49590f..e9e3e83 100644 --- a/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivity.kt +++ b/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivity.kt @@ -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) } } } diff --git a/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivityExt.kt b/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivityExt.kt new file mode 100644 index 0000000..b955307 --- /dev/null +++ b/app/src/main/java/com/afollestad/nocknock/ui/main/MainActivityExt.kt @@ -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) + } +} diff --git a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt index ae57fa9..59d2a69 100644 --- a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt +++ b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivity.kt @@ -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() diff --git a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivityExt.kt b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivityExt.kt new file mode 100644 index 0000000..41858c4 --- /dev/null +++ b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSiteActivityExt.kt @@ -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 + } + } +} diff --git a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSitePresenter.kt b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSitePresenter.kt index 42d8c26..376b3c0 100644 --- a/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSitePresenter.kt +++ b/app/src/main/java/com/afollestad/nocknock/ui/viewsite/ViewSitePresenter.kt @@ -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 ) diff --git a/app/src/main/res/layout/menu_item_refresh_icon.xml b/app/src/main/res/layout/menu_item_refresh_icon.xml new file mode 100644 index 0000000..0079df9 --- /dev/null +++ b/app/src/main/res/layout/menu_item_refresh_icon.xml @@ -0,0 +1,9 @@ + + diff --git a/app/src/test/java/com/afollestad/nocknock/ViewSitePresenterTest.kt b/app/src/test/java/com/afollestad/nocknock/ViewSitePresenterTest.kt index 5678bb1..5ad8e3b 100644 --- a/app/src/test/java/com/afollestad/nocknock/ViewSitePresenterTest.kt +++ b/app/src/test/java/com/afollestad/nocknock/ViewSitePresenterTest.kt @@ -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, diff --git a/data/src/main/java/com/afollestad/nocknock/data/ServerModel.kt b/data/src/main/java/com/afollestad/nocknock/data/ServerModel.kt index 061d851..ecba970 100644 --- a/data/src/main/java/com/afollestad/nocknock/data/ServerModel.kt +++ b/data/src/main/java/com/afollestad/nocknock/data/ServerModel.kt @@ -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 diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/db/ServerModelStore.kt b/engine/src/main/java/com/afollestad/nocknock/engine/db/ServerModelStore.kt index 8d5e719..e5050d2 100644 --- a/engine/src/main/java/com/afollestad/nocknock/engine/db/ServerModelStore.kt +++ b/engine/src/main/java/com/afollestad/nocknock/engine/db/ServerModelStore.kt @@ -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) } diff --git a/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusJob.kt b/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusJob.kt index c457f0d..0a7d3fe 100644 --- a/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusJob.kt +++ b/engine/src/main/java/com/afollestad/nocknock/engine/statuscheck/CheckStatusJob.kt @@ -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, diff --git a/notifications/src/main/java/com/afollestad/nocknock/notifications/Channel.kt b/notifications/src/main/java/com/afollestad/nocknock/notifications/Channel.kt index 187a78c..4865efc 100644 --- a/notifications/src/main/java/com/afollestad/nocknock/notifications/Channel.kt +++ b/notifications/src/main/java/com/afollestad/nocknock/notifications/Channel.kt @@ -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 ) } diff --git a/notifications/src/main/java/com/afollestad/nocknock/notifications/NockNotificationManager.kt b/notifications/src/main/java/com/afollestad/nocknock/notifications/NockNotificationManager.kt index f32c97d..295c2ec 100644 --- a/notifications/src/main/java/com/afollestad/nocknock/notifications/NockNotificationManager.kt +++ b/notifications/src/main/java/com/afollestad/nocknock/notifications/NockNotificationManager.kt @@ -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 } diff --git a/notifications/src/main/res/values/strings.xml b/notifications/src/main/res/values/strings.xml index 8b1e992..ba78f43 100644 --- a/notifications/src/main/res/values/strings.xml +++ b/notifications/src/main/res/values/strings.xml @@ -1,9 +1,10 @@ - Server Statuses - - Notifications for server status changes, whether it\'s successful statuses or error statuses. + Server Check Failures + + Notifications for Nock Nock status checks failing for your sites. Something has gone + wrong if you see one of these. Something\'s wrong! Tap for details. diff --git a/utilities/src/main/java/com/afollestad/nocknock/utilities/UtilitiesModule.kt b/utilities/src/main/java/com/afollestad/nocknock/utilities/UtilitiesModule.kt index 5d8aaa7..af4a35a 100644 --- a/utilities/src/main/java/com/afollestad/nocknock/utilities/UtilitiesModule.kt +++ b/utilities/src/main/java/com/afollestad/nocknock/utilities/UtilitiesModule.kt @@ -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 } diff --git a/utilities/src/main/java/com/afollestad/nocknock/utilities/ext/AnimationExt.kt b/utilities/src/main/java/com/afollestad/nocknock/utilities/ext/AnimationExt.kt index a337283..2a54b19 100644 --- a/utilities/src/main/java/com/afollestad/nocknock/utilities/ext/AnimationExt.kt +++ b/utilities/src/main/java/com/afollestad/nocknock/utilities/ext/AnimationExt.kt @@ -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() +} diff --git a/utilities/src/main/java/com/afollestad/nocknock/utilities/providers/IntentProvider.kt b/utilities/src/main/java/com/afollestad/nocknock/utilities/providers/IntentProvider.kt new file mode 100644 index 0000000..6fe0966 --- /dev/null +++ b/utilities/src/main/java/com/afollestad/nocknock/utilities/providers/IntentProvider.kt @@ -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) + } +} diff --git a/utilities/src/main/java/com/afollestad/nocknock/utilities/qualifiers/MainActivityClass.kt b/utilities/src/main/java/com/afollestad/nocknock/utilities/qualifiers/MainActivityClass.kt new file mode 100644 index 0000000..9db4598 --- /dev/null +++ b/utilities/src/main/java/com/afollestad/nocknock/utilities/qualifiers/MainActivityClass.kt @@ -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 diff --git a/viewcomponents/src/main/res/layout/javascript_input_layout.xml b/viewcomponents/src/main/res/layout/javascript_input_layout.xml index 49f282c..b1d786a 100644 --- a/viewcomponents/src/main/res/layout/javascript_input_layout.xml +++ b/viewcomponents/src/main/res/layout/javascript_input_layout.xml @@ -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"