Bug fixes, UI improvements, fix notifications, etc.

This commit is contained in:
Aidan Follestad 2018-12-01 15:58:24 -08:00
parent b750957d79
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.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)

View file

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

View file

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

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

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.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()

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) {
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
)

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)
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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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: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"