2021-10-28 16:04:14 +13:00
package io.heckel.ntfy.ui
2021-10-26 02:01:10 +13:00
2021-11-04 05:48:13 +13:00
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.app.AlertDialog
2021-10-26 02:01:10 +13:00
import android.content.Intent
2021-10-28 16:04:14 +13:00
import android.net.Uri
2022-01-19 10:49:00 +13:00
import android.os.Build
2021-10-26 02:01:10 +13:00
import android.os.Bundle
2022-01-19 10:49:00 +13:00
import android.provider.Settings
2022-01-17 18:19:05 +13:00
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.view.View
2022-01-19 10:49:00 +13:00
import android.widget.Button
2021-11-08 07:29:19 +13:00
import android.widget.Toast
2021-10-26 02:01:10 +13:00
import androidx.activity.viewModels
2021-10-26 07:24:44 +13:00
import androidx.appcompat.app.AppCompatActivity
2021-11-04 05:48:13 +13:00
import androidx.core.content.ContextCompat
2021-11-08 07:29:19 +13:00
import androidx.lifecycle.lifecycleScope
2021-10-26 02:01:10 +13:00
import androidx.recyclerview.widget.RecyclerView
2021-11-15 08:20:30 +13:00
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
2021-11-12 13:41:29 +13:00
import androidx.work.*
2021-10-31 13:16:12 +13:00
import io.heckel.ntfy.R
2021-10-30 14:13:58 +13:00
import io.heckel.ntfy.app.Application
2022-01-19 10:49:00 +13:00
import io.heckel.ntfy.db.Repository
2022-01-19 08:28:48 +13:00
import io.heckel.ntfy.db.Subscription
2021-11-25 10:12:51 +13:00
import io.heckel.ntfy.firebase.FirebaseMessenger
2022-01-17 18:19:05 +13:00
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationDispatcher
2021-12-31 05:00:27 +13:00
import io.heckel.ntfy.service.SubscriberService
import io.heckel.ntfy.service.SubscriberServiceManager
2022-01-19 10:49:00 +13:00
import io.heckel.ntfy.util.*
2022-01-17 18:19:05 +13:00
import io.heckel.ntfy.work.PollWorker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
2021-11-01 08:19:25 +13:00
import java.util.*
2021-11-12 13:41:29 +13:00
import java.util.concurrent.TimeUnit
2021-10-31 13:16:12 +13:00
import kotlin.random.Random
2021-10-26 02:01:10 +13:00
2021-11-23 09:45:43 +13:00
class MainActivity : AppCompatActivity ( ) , ActionMode . Callback , AddFragment . SubscribeListener , NotificationFragment . NotificationSettingsListener {
2021-11-01 08:19:25 +13:00
private val viewModel by viewModels < SubscriptionsViewModel > {
2021-10-30 14:13:58 +13:00
SubscriptionsViewModelFactory ( ( application as Application ) . repository )
}
2021-11-08 07:29:19 +13:00
private val repository by lazy { ( application as Application ) . repository }
2021-11-12 13:41:29 +13:00
private val api = ApiService ( )
2021-11-25 10:12:51 +13:00
private val messenger = FirebaseMessenger ( )
2021-11-12 13:41:29 +13:00
2021-11-23 09:45:43 +13:00
// UI elements
private lateinit var menu : Menu
2021-11-04 05:48:13 +13:00
private lateinit var mainList : RecyclerView
2021-11-15 08:20:30 +13:00
private lateinit var mainListContainer : SwipeRefreshLayout
2021-11-04 05:48:13 +13:00
private lateinit var adapter : MainAdapter
private lateinit var fab : View
2021-11-23 09:45:43 +13:00
// Other stuff
2021-11-04 05:48:13 +13:00
private var actionMode : ActionMode ? = null
2021-11-12 13:41:29 +13:00
private var workManager : WorkManager ? = null // Context-dependent
2021-12-30 08:33:17 +13:00
private var dispatcher : NotificationDispatcher ? = null // Context-dependent
2021-11-15 11:05:35 +13:00
private var appBaseUrl : String ? = null // Context-dependent
2021-10-30 14:13:58 +13:00
2021-10-26 02:01:10 +13:00
override fun onCreate ( savedInstanceState : Bundle ? ) {
super . onCreate ( savedInstanceState )
2021-11-23 09:45:43 +13:00
setContentView ( R . layout . activity _main )
2021-10-26 02:01:10 +13:00
2022-01-18 12:05:59 +13:00
Log . init ( this ) // Init logs in all entry points
2021-11-12 13:41:29 +13:00
Log . d ( TAG , " Create $this " )
2021-11-08 07:29:19 +13:00
// Dependencies that depend on Context
2021-11-12 13:41:29 +13:00
workManager = WorkManager . getInstance ( this )
2021-12-30 09:36:47 +13:00
dispatcher = NotificationDispatcher ( this , repository )
2021-11-15 11:05:35 +13:00
appBaseUrl = getString ( R . string . app _base _url )
2021-11-08 07:29:19 +13:00
2021-10-29 04:45:34 +13:00
// Action bar
title = getString ( R . string . main _action _bar _title )
2021-10-27 08:55:59 +13:00
// Floating action button ("+")
2021-11-04 05:48:13 +13:00
fab = findViewById ( R . id . fab )
2021-10-27 08:55:59 +13:00
fab . setOnClickListener {
2021-10-30 14:13:58 +13:00
onSubscribeButtonClick ( )
2021-10-27 08:55:59 +13:00
}
2021-11-15 10:48:50 +13:00
// Swipe to refresh
mainListContainer = findViewById ( R . id . main _subscriptions _list _container )
2021-11-15 08:20:30 +13:00
mainListContainer . setOnRefreshListener { refreshAllSubscriptions ( ) }
mainListContainer . setColorSchemeResources ( R . color . primaryColor )
2021-11-01 08:19:25 +13:00
// Update main list based on viewModel (& its datasource/livedata)
2021-11-02 03:49:52 +13:00
val noEntries : View = findViewById ( R . id . main _no _subscriptions )
2021-11-04 05:48:13 +13:00
val onSubscriptionClick = { s : Subscription -> onSubscriptionItemClick ( s ) }
val onSubscriptionLongClick = { s : Subscription -> onSubscriptionItemLongClick ( s ) }
mainList = findViewById ( R . id . main _subscriptions _list )
2022-01-02 04:56:18 +13:00
adapter = MainAdapter ( repository , onSubscriptionClick , onSubscriptionLongClick )
2021-10-29 04:45:34 +13:00
mainList . adapter = adapter
2021-10-26 02:01:10 +13:00
2021-11-01 08:19:25 +13:00
viewModel . list ( ) . observe ( this ) {
2021-11-12 16:14:28 +13:00
it ?. let { subscriptions ->
2022-01-18 12:05:59 +13:00
// Update main list
2021-11-12 16:14:28 +13:00
adapter . submitList ( subscriptions as MutableList < Subscription > )
2021-10-29 04:45:34 +13:00
if ( it . isEmpty ( ) ) {
2021-11-15 08:20:30 +13:00
mainListContainer . visibility = View . GONE
2021-11-02 03:49:52 +13:00
noEntries . visibility = View . VISIBLE
2021-10-29 04:45:34 +13:00
} else {
2021-11-15 08:20:30 +13:00
mainListContainer . visibility = View . VISIBLE
2021-11-02 03:49:52 +13:00
noEntries . visibility = View . GONE
2021-10-29 04:45:34 +13:00
}
2022-01-18 12:05:59 +13:00
// Add scrub terms to log (in case it gets exported)
subscriptions . forEach { s ->
Log . addScrubTerm ( shortUrl ( s . baseUrl ) , Log . TermType . Domain )
Log . addScrubTerm ( s . topic )
}
2021-10-26 02:01:10 +13:00
}
2021-10-26 06:45:56 +13:00
}
2021-11-12 13:41:29 +13:00
2021-12-30 11:48:06 +13:00
// React to changes in instant delivery setting
2021-11-15 07:54:48 +13:00
viewModel . listIdsWithInstantStatus ( ) . observe ( this ) {
2021-12-31 14:00:08 +13:00
SubscriberServiceManager . refresh ( this )
2021-11-14 13:26:37 +13:00
}
2022-01-19 10:49:00 +13:00
// Battery banner
2022-01-19 13:10:34 +13:00
val batteryBanner = findViewById < View > ( R . id . main _banner _battery ) // Banner visibility is toggled in onResume()
val dontAskAgainButton = findViewById < Button > ( R . id . main _banner _battery _dontaskagain )
val askLaterButton = findViewById < Button > ( R . id . main _banner _battery _ask _later )
val fixNowButton = findViewById < Button > ( R . id . main _banner _battery _fix _now )
dontAskAgainButton . setOnClickListener {
batteryBanner . visibility = View . GONE
repository . setBatteryOptimizationsRemindTime ( Repository . BATTERY _OPTIMIZATIONS _REMIND _TIME _NEVER )
}
askLaterButton . setOnClickListener {
batteryBanner . visibility = View . GONE
repository . setBatteryOptimizationsRemindTime ( System . currentTimeMillis ( ) + ONE _DAY _MILLIS )
}
fixNowButton . setOnClickListener {
if ( Build . VERSION . SDK _INT >= Build . VERSION_CODES . M ) {
val intent = Intent ( Settings . ACTION _IGNORE _BATTERY _OPTIMIZATION _SETTINGS )
startActivity ( intent )
2022-01-19 10:49:00 +13:00
}
}
2021-11-30 08:06:08 +13:00
// Create notification channels right away, so we can configure them immediately after installing the app
2021-12-30 09:36:47 +13:00
dispatcher ?. init ( )
2021-11-30 08:06:08 +13:00
2021-12-14 14:54:36 +13:00
// Subscribe to control Firebase channel (so we can re-start the foreground service if it dies)
2021-12-14 15:10:48 +13:00
messenger . subscribe ( ApiService . CONTROL _TOPIC )
2021-12-14 14:54:36 +13:00
2021-11-14 13:26:37 +13:00
// Background things
2021-12-14 14:54:36 +13:00
startPeriodicPollWorker ( )
2022-01-02 13:37:09 +13:00
startPeriodicServiceRestartWorker ( )
2022-01-06 09:40:40 +13:00
}
2022-01-12 11:00:18 +13:00
2022-01-02 13:37:09 +13:00
override fun onResume ( ) {
super . onResume ( )
2022-01-19 13:10:34 +13:00
// Menu and main list
2022-01-02 13:37:09 +13:00
showHideNotificationMenuItems ( )
redrawList ( )
2022-01-19 13:10:34 +13:00
// Battery banner
val batteryRemindTimeReached = repository . getBatteryOptimizationsRemindTime ( ) < System . currentTimeMillis ( )
val ignoringBatteryOptimizations = isIgnoringBatteryOptimizations ( this )
val showBatteryBanner = batteryRemindTimeReached && ! ignoringBatteryOptimizations
val batteryBanner = findViewById < View > ( R . id . main _banner _battery )
batteryBanner . visibility = if ( showBatteryBanner ) View . VISIBLE else View . GONE
Log . d ( TAG , " Battery: ignoring optimizations = $ignoringBatteryOptimizations (we want this to be true); remind time reached = $batteryRemindTimeReached " )
2021-11-14 13:26:37 +13:00
}
2021-12-14 14:54:36 +13:00
private fun startPeriodicPollWorker ( ) {
val workerVersion = repository . getPollWorkerVersion ( )
val workPolicy = if ( workerVersion == PollWorker . VERSION ) {
2021-11-12 13:41:29 +13:00
Log . d ( TAG , " Poll worker version matches: choosing KEEP as existing work policy " )
ExistingPeriodicWorkPolicy . KEEP
} else {
Log . d ( TAG , " Poll worker version DOES NOT MATCH: choosing REPLACE as existing work policy " )
2021-11-23 09:45:43 +13:00
repository . setPollWorkerVersion ( PollWorker . VERSION )
2021-11-12 13:41:29 +13:00
ExistingPeriodicWorkPolicy . REPLACE
}
val constraints = Constraints . Builder ( )
. setRequiredNetworkType ( NetworkType . CONNECTED )
. build ( )
2022-01-02 13:37:09 +13:00
val work = PeriodicWorkRequestBuilder < PollWorker > ( POLL _WORKER _INTERVAL _MINUTES , TimeUnit . MINUTES )
2021-11-12 13:41:29 +13:00
. setConstraints ( constraints )
. addTag ( PollWorker . TAG )
. addTag ( PollWorker . WORK _NAME _PERIODIC )
. build ( )
2022-01-02 13:37:09 +13:00
Log . d ( TAG , " Poll worker: Scheduling period work every $POLL _WORKER_INTERVAL_MINUTES minutes " )
2021-11-12 13:41:29 +13:00
workManager !! . enqueueUniquePeriodicWork ( PollWorker . WORK _NAME _PERIODIC , workPolicy , work )
2021-10-26 14:14:09 +13:00
}
2021-10-26 13:25:54 +13:00
2022-01-02 13:37:09 +13:00
private fun startPeriodicServiceRestartWorker ( ) {
2021-12-14 14:54:36 +13:00
val workerVersion = repository . getAutoRestartWorkerVersion ( )
2022-01-02 13:37:09 +13:00
val workPolicy = if ( workerVersion == SubscriberService . SERVICE _START _WORKER _VERSION ) {
Log . d ( TAG , " ServiceStartWorker version matches: choosing KEEP as existing work policy " )
2021-12-14 14:54:36 +13:00
ExistingPeriodicWorkPolicy . KEEP
} else {
2022-01-02 13:37:09 +13:00
Log . d ( TAG , " ServiceStartWorker version DOES NOT MATCH: choosing REPLACE as existing work policy " )
repository . setAutoRestartWorkerVersion ( SubscriberService . SERVICE _START _WORKER _VERSION )
2021-12-14 14:54:36 +13:00
ExistingPeriodicWorkPolicy . REPLACE
}
2022-01-02 13:37:09 +13:00
val work = PeriodicWorkRequestBuilder < SubscriberServiceManager . ServiceStartWorker > ( SERVICE _START _WORKER _INTERVAL _MINUTES , TimeUnit . MINUTES )
2021-12-14 14:54:36 +13:00
. addTag ( SubscriberService . TAG )
2022-01-02 13:37:09 +13:00
. addTag ( SubscriberService . SERVICE _START _WORKER _WORK _NAME _PERIODIC )
2021-12-14 14:54:36 +13:00
. build ( )
2022-01-02 13:37:09 +13:00
Log . d ( TAG , " ServiceStartWorker: Scheduling period work every $SERVICE _START_WORKER_INTERVAL_MINUTES minutes " )
workManager ?. enqueueUniquePeriodicWork ( SubscriberService . SERVICE _START _WORKER _WORK _NAME _PERIODIC , workPolicy , work )
2021-12-14 14:54:36 +13:00
}
2021-10-28 16:04:14 +13:00
override fun onCreateOptionsMenu ( menu : Menu ) : Boolean {
2021-11-23 09:45:43 +13:00
menuInflater . inflate ( R . menu . menu _main _action _bar , menu )
this . menu = menu
showHideNotificationMenuItems ( )
2022-01-02 13:37:09 +13:00
checkSubscriptionsMuted ( ) // This is done here, because then we know that we've initialized the menu
2021-10-28 16:04:14 +13:00
return true
}
2022-01-02 13:37:09 +13:00
private fun checkSubscriptionsMuted ( delayMillis : Long = 0L ) {
2021-11-23 09:45:43 +13:00
lifecycleScope . launch ( Dispatchers . IO ) {
2022-01-02 13:37:09 +13:00
delay ( delayMillis ) // Just to be sure we've initialized all the things, we wait a bit ...
Log . d ( TAG , " Checking global and subscription-specific 'muted until' timestamp " )
// Check global
val changed = repository . checkGlobalMutedUntil ( )
if ( changed ) {
Log . d ( TAG , " Global muted until timestamp expired; updating prefs " )
showHideNotificationMenuItems ( )
}
2021-11-23 09:45:43 +13:00
2022-01-02 13:37:09 +13:00
// Check subscriptions
var rerenderList = false
repository . getSubscriptions ( ) . forEach { subscription ->
val mutedUntilExpired = subscription . mutedUntil > 1L && System . currentTimeMillis ( ) / 1000 > subscription . mutedUntil
if ( mutedUntilExpired ) {
Log . d ( TAG , " Subscription ${subscription.id} : Muted until timestamp expired, updating subscription " )
val newSubscription = subscription . copy ( mutedUntil = 0L )
repository . updateSubscription ( newSubscription )
rerenderList = true
2021-11-23 09:45:43 +13:00
}
2022-01-02 13:37:09 +13:00
}
if ( rerenderList ) {
redrawList ( )
2021-11-23 09:45:43 +13:00
}
}
}
private fun showHideNotificationMenuItems ( ) {
2022-01-02 13:37:09 +13:00
if ( ! this :: menu . isInitialized ) {
return
}
2021-11-23 09:45:43 +13:00
val mutedUntilSeconds = repository . getGlobalMutedUntil ( )
runOnUiThread {
val notificationsEnabledItem = menu . findItem ( R . id . main _menu _notifications _enabled )
val notificationsDisabledUntilItem = menu . findItem ( R . id . main _menu _notifications _disabled _until )
val notificationsDisabledForeverItem = menu . findItem ( R . id . main _menu _notifications _disabled _forever )
notificationsEnabledItem ?. isVisible = mutedUntilSeconds == 0L
notificationsDisabledForeverItem ?. isVisible = mutedUntilSeconds == 1L
notificationsDisabledUntilItem ?. isVisible = mutedUntilSeconds > 1L
if ( mutedUntilSeconds > 1L ) {
val formattedDate = formatDateShort ( mutedUntilSeconds )
notificationsDisabledUntilItem ?. title = getString ( R . string . main _menu _notifications _disabled _until , formattedDate )
}
}
}
2021-10-28 16:04:14 +13:00
override fun onOptionsItemSelected ( item : MenuItem ) : Boolean {
return when ( item . itemId ) {
2021-11-23 09:45:43 +13:00
R . id . main _menu _notifications _enabled -> {
onNotificationSettingsClick ( enable = false )
true
}
R . id . main _menu _notifications _disabled _forever -> {
onNotificationSettingsClick ( enable = true )
true
}
R . id . main _menu _notifications _disabled _until -> {
onNotificationSettingsClick ( enable = true )
true
}
2021-12-31 13:34:25 +13:00
R . id . main _menu _settings -> {
startActivity ( Intent ( this , SettingsActivity :: class . java ) )
true
}
2021-11-02 02:57:05 +13:00
R . id . main _menu _source -> {
2021-10-29 01:28:22 +13:00
startActivity ( Intent ( Intent . ACTION _VIEW , Uri . parse ( getString ( R . string . main _menu _source _url ) ) ) )
2021-10-28 16:04:14 +13:00
true
}
2021-11-02 02:57:05 +13:00
R . id . main _menu _website -> {
2021-11-15 11:05:35 +13:00
startActivity ( Intent ( Intent . ACTION _VIEW , Uri . parse ( appBaseUrl ) ) )
2021-10-28 16:04:14 +13:00
true
}
else -> super . onOptionsItemSelected ( item )
}
}
2021-11-23 09:45:43 +13:00
private fun onNotificationSettingsClick ( enable : Boolean ) {
if ( ! enable ) {
Log . d ( TAG , " Showing global notification settings dialog " )
val notificationFragment = NotificationFragment ( )
notificationFragment . show ( supportFragmentManager , NotificationFragment . TAG )
} else {
Log . d ( TAG , " Re-enabling global notifications " )
onNotificationMutedUntilChanged ( 0L )
}
}
override fun onNotificationMutedUntilChanged ( mutedUntilTimestamp : Long ) {
repository . setGlobalMutedUntil ( mutedUntilTimestamp )
showHideNotificationMenuItems ( )
runOnUiThread {
2022-01-02 04:56:18 +13:00
redrawList ( ) // Update the "muted until" icons
2021-11-23 09:45:43 +13:00
when ( mutedUntilTimestamp ) {
0L -> Toast . makeText ( this @MainActivity , getString ( R . string . notification _dialog _enabled _toast _message ) , Toast . LENGTH _LONG ) . show ( )
1L -> Toast . makeText ( this @MainActivity , getString ( R . string . notification _dialog _muted _forever _toast _message ) , Toast . LENGTH _LONG ) . show ( )
else -> {
val formattedDate = formatDateShort ( mutedUntilTimestamp )
Toast . makeText ( this @MainActivity , getString ( R . string . notification _dialog _muted _until _toast _message , formattedDate ) , Toast . LENGTH _LONG ) . show ( )
}
}
}
}
2021-10-30 14:13:58 +13:00
private fun onSubscribeButtonClick ( ) {
2021-11-18 14:16:58 +13:00
val newFragment = AddFragment ( )
newFragment . show ( supportFragmentManager , AddFragment . TAG )
2021-10-26 02:01:10 +13:00
}
2021-11-18 14:16:58 +13:00
override fun onSubscribe ( topic : String , baseUrl : String , instant : Boolean ) {
2021-11-02 02:57:05 +13:00
Log . d ( TAG , " Adding subscription ${topicShortUrl(baseUrl, topic)} " )
2021-11-08 07:29:19 +13:00
// Add subscription to database
2021-11-04 05:48:13 +13:00
val subscription = Subscription (
id = Random . nextLong ( ) ,
baseUrl = baseUrl ,
topic = topic ,
2021-11-14 13:26:37 +13:00
instant = instant ,
2021-11-22 08:54:13 +13:00
mutedUntil = 0 ,
2021-12-31 02:23:47 +13:00
upAppId = null ,
upConnectorToken = null ,
2021-11-16 10:24:31 +13:00
totalCount = 0 ,
newCount = 0 ,
2021-11-04 05:48:13 +13:00
lastActive = Date ( ) . time / 1000
)
2021-11-01 08:19:25 +13:00
viewModel . add ( subscription )
2021-11-08 07:29:19 +13:00
2021-11-15 11:05:35 +13:00
// Subscribe to Firebase topic if ntfy.sh (even if instant, just to be sure!)
if ( baseUrl == appBaseUrl ) {
2021-11-14 13:26:37 +13:00
Log . d ( TAG , " Subscribing to Firebase " )
2021-11-25 10:12:51 +13:00
messenger . subscribe ( topic )
2021-11-14 13:26:37 +13:00
}
2021-11-06 13:25:02 +13:00
2021-11-08 07:29:19 +13:00
// Fetch cached messages
2021-11-12 13:41:29 +13:00
lifecycleScope . launch ( Dispatchers . IO ) {
try {
val notifications = api . poll ( subscription . id , subscription . baseUrl , subscription . topic )
2021-11-12 16:14:28 +13:00
notifications . forEach { notification -> repository . addNotification ( notification ) }
2021-11-12 13:41:29 +13:00
} catch ( e : Exception ) {
Log . e ( TAG , " Unable to fetch notifications: ${e.stackTrace} " )
2021-11-08 07:29:19 +13:00
}
}
2021-11-03 14:43:31 +13:00
2021-11-08 07:29:19 +13:00
// Switch to detail view after adding it
onSubscriptionItemClick ( subscription )
2021-10-26 02:01:10 +13:00
}
2021-10-26 13:25:54 +13:00
2021-11-01 08:19:25 +13:00
private fun onSubscriptionItemClick ( subscription : Subscription ) {
2021-11-04 05:48:13 +13:00
if ( actionMode != null ) {
handleActionModeClick ( subscription )
2021-12-31 02:23:47 +13:00
} else if ( subscription . upAppId != null ) { // Not UnifiedPush
displayUnifiedPushToast ( subscription )
2021-11-04 05:48:13 +13:00
} else {
startDetailView ( subscription )
}
}
2021-12-31 02:23:47 +13:00
private fun displayUnifiedPushToast ( subscription : Subscription ) {
runOnUiThread {
2021-12-31 05:00:27 +13:00
val appId = subscription . upAppId ?: return @runOnUiThread
2021-12-31 02:23:47 +13:00
val toastMessage = getString ( R . string . main _unified _push _toast , appId )
Toast . makeText ( this @MainActivity , toastMessage , Toast . LENGTH _LONG ) . show ( )
}
}
2021-11-04 05:48:13 +13:00
private fun onSubscriptionItemLongClick ( subscription : Subscription ) {
if ( actionMode == null ) {
beginActionMode ( subscription )
}
}
2021-11-12 13:41:29 +13:00
private fun refreshAllSubscriptions ( ) {
2021-11-11 15:16:00 +13:00
lifecycleScope . launch ( Dispatchers . IO ) {
2021-12-12 13:40:32 +13:00
Log . d ( TAG , " Polling for new notifications " )
var errors = 0
var errorMessage = " " // First error
var newNotificationsCount = 0
repository . getSubscriptions ( ) . forEach { subscription ->
try {
2021-11-12 13:41:29 +13:00
val notifications = api . poll ( subscription . id , subscription . baseUrl , subscription . topic )
val newNotifications = repository . onlyNewNotifications ( subscription . id , notifications )
newNotifications . forEach { notification ->
2021-11-12 16:14:28 +13:00
newNotificationsCount ++
2021-11-23 09:45:43 +13:00
val notificationWithId = notification . copy ( notificationId = Random . nextInt ( ) )
2021-12-30 09:36:47 +13:00
if ( repository . addNotification ( notificationWithId ) ) {
dispatcher ?. dispatch ( subscription , notificationWithId )
2021-12-12 09:09:07 +13:00
}
2021-11-11 15:16:00 +13:00
}
2021-12-12 13:40:32 +13:00
} catch ( e : Exception ) {
val topic = topicShortUrl ( subscription . baseUrl , subscription . topic )
if ( errorMessage == " " ) errorMessage = " $topic : ${e.message} "
errors ++
2021-11-11 15:16:00 +13:00
}
}
2021-12-12 13:40:32 +13:00
val toastMessage = if ( errors > 0 ) {
getString ( R . string . refresh _message _error , errors , errorMessage )
} else if ( newNotificationsCount == 0 ) {
getString ( R . string . refresh _message _no _results )
} else {
getString ( R . string . refresh _message _result , newNotificationsCount )
}
runOnUiThread {
Toast . makeText ( this @MainActivity , toastMessage , Toast . LENGTH _LONG ) . show ( )
mainListContainer . isRefreshing = false
}
Log . d ( TAG , " Finished polling for new notifications " )
2021-11-11 15:16:00 +13:00
}
}
2021-11-04 05:48:13 +13:00
private fun startDetailView ( subscription : Subscription ) {
2021-11-02 02:57:05 +13:00
Log . d ( TAG , " Entering detail view for subscription $subscription " )
2021-11-01 08:19:25 +13:00
val intent = Intent ( this , DetailActivity :: class . java )
intent . putExtra ( EXTRA _SUBSCRIPTION _ID , subscription . id )
intent . putExtra ( EXTRA _SUBSCRIPTION _BASE _URL , subscription . baseUrl )
intent . putExtra ( EXTRA _SUBSCRIPTION _TOPIC , subscription . topic )
2021-11-14 13:26:37 +13:00
intent . putExtra ( EXTRA _SUBSCRIPTION _INSTANT , subscription . instant )
2021-11-23 09:45:43 +13:00
intent . putExtra ( EXTRA _SUBSCRIPTION _MUTED _UNTIL , subscription . mutedUntil )
2021-11-27 09:51:05 +13:00
startActivity ( intent )
2021-10-27 05:23:41 +13:00
}
2021-10-27 06:46:49 +13:00
2021-11-04 05:48:13 +13:00
private fun handleActionModeClick ( subscription : Subscription ) {
adapter . toggleSelection ( subscription . id )
if ( adapter . selected . size == 0 ) {
finishActionMode ( )
} else {
actionMode !! . title = adapter . selected . size . toString ( )
redrawList ( )
}
}
override fun onCreateActionMode ( mode : ActionMode ? , menu : Menu ? ) : Boolean {
this . actionMode = mode
if ( mode != null ) {
2021-11-23 09:45:43 +13:00
mode . menuInflater . inflate ( R . menu . menu _main _action _mode , menu )
2021-11-04 05:48:13 +13:00
mode . title = " 1 " // One item selected
}
return true
}
override fun onPrepareActionMode ( mode : ActionMode ? , menu : Menu ? ) : Boolean {
return false
}
override fun onActionItemClicked ( mode : ActionMode ? , item : MenuItem ? ) : Boolean {
return when ( item ?. itemId ) {
2021-11-04 06:56:08 +13:00
R . id . main _action _mode _delete -> {
2021-11-04 05:48:13 +13:00
onMultiDeleteClick ( )
true
}
else -> false
}
}
private fun onMultiDeleteClick ( ) {
Log . d ( DetailActivity . TAG , " Showing multi-delete dialog for selected items " )
val builder = AlertDialog . Builder ( this )
2021-11-26 03:26:37 +13:00
val dialog = builder
2021-11-04 05:48:13 +13:00
. setMessage ( R . string . main _action _mode _delete _dialog _message )
. setPositiveButton ( R . string . main _action _mode _delete _dialog _permanently _delete ) { _ , _ ->
2021-12-31 02:23:47 +13:00
adapter . selected . map { subscriptionId -> viewModel . remove ( this , subscriptionId ) }
2021-11-04 05:48:13 +13:00
finishActionMode ( )
}
. setNegativeButton ( R . string . main _action _mode _delete _dialog _cancel ) { _ , _ ->
finishActionMode ( )
}
. create ( )
2021-11-26 03:26:37 +13:00
dialog . setOnShowListener {
dialog
. getButton ( AlertDialog . BUTTON _POSITIVE )
. setTextColor ( ContextCompat . getColor ( this , R . color . primaryDangerButtonColor ) )
}
dialog . show ( )
2021-11-04 05:48:13 +13:00
}
override fun onDestroyActionMode ( mode : ActionMode ? ) {
endActionModeAndRedraw ( )
}
private fun beginActionMode ( subscription : Subscription ) {
actionMode = startActionMode ( this )
adapter . selected . add ( subscription . id )
redrawList ( )
// Fade out FAB
fab . alpha = 1f
fab
. animate ( )
. alpha ( 0f )
. setDuration ( ANIMATION _DURATION )
. setListener ( object : AnimatorListenerAdapter ( ) {
override fun onAnimationEnd ( animation : Animator ) {
fab . visibility = View . GONE
}
} )
// Fade status bar color
val fromColor = ContextCompat . getColor ( this , R . color . primaryColor )
val toColor = ContextCompat . getColor ( this , R . color . primaryDarkColor )
2021-11-04 06:56:08 +13:00
fadeStatusBarColor ( window , fromColor , toColor )
2021-11-04 05:48:13 +13:00
}
private fun finishActionMode ( ) {
actionMode !! . finish ( )
endActionModeAndRedraw ( )
}
private fun endActionModeAndRedraw ( ) {
actionMode = null
adapter . selected . clear ( )
redrawList ( )
// Fade in FAB
fab . alpha = 0f
fab . visibility = View . VISIBLE
fab
. animate ( )
. alpha ( 1f )
. setDuration ( ANIMATION _DURATION )
. setListener ( object : AnimatorListenerAdapter ( ) {
override fun onAnimationEnd ( animation : Animator ) {
fab . visibility = View . VISIBLE // Required to replace the old listener
}
} )
// Fade status bar color
val fromColor = ContextCompat . getColor ( this , R . color . primaryDarkColor )
val toColor = ContextCompat . getColor ( this , R . color . primaryColor )
2021-11-04 06:56:08 +13:00
fadeStatusBarColor ( window , fromColor , toColor )
2021-11-04 05:48:13 +13:00
}
private fun redrawList ( ) {
2022-01-02 13:37:09 +13:00
if ( ! this :: mainList . isInitialized ) {
return
}
2021-11-23 09:45:43 +13:00
runOnUiThread {
mainList . adapter = adapter // Oh, what a hack ...
}
2021-11-04 05:48:13 +13:00
}
2021-10-30 14:13:58 +13:00
companion object {
const val TAG = " NtfyMainActivity "
2021-11-01 08:19:25 +13:00
const val EXTRA _SUBSCRIPTION _ID = " subscriptionId "
const val EXTRA _SUBSCRIPTION _BASE _URL = " subscriptionBaseUrl "
const val EXTRA _SUBSCRIPTION _TOPIC = " subscriptionTopic "
2021-11-14 13:26:37 +13:00
const val EXTRA _SUBSCRIPTION _INSTANT = " subscriptionInstant "
2021-11-23 09:45:43 +13:00
const val EXTRA _SUBSCRIPTION _MUTED _UNTIL = " subscriptionMutedUntil "
2021-11-04 05:48:13 +13:00
const val ANIMATION _DURATION = 80L
2022-01-19 10:49:00 +13:00
const val ONE _DAY _MILLIS = 86400000L
2021-12-14 14:54:36 +13:00
2022-01-02 13:37:09 +13:00
// As per documentation: The minimum repeat interval that can be defined is 15 minutes
2021-12-14 14:54:36 +13:00
// (same as the JobScheduler API), but in practice 15 doesn't work. Using 16 here.
// Thanks to varunon9 (https://gist.github.com/varunon9/f2beec0a743c96708eb0ef971a9ff9cd) for this!
2022-01-02 13:37:09 +13:00
const val POLL _WORKER _INTERVAL _MINUTES = 2 * 60L
2022-01-18 13:02:46 +13:00
const val SERVICE _START _WORKER _INTERVAL _MINUTES = 3 * 60L
2021-10-27 06:46:49 +13:00
}
2021-10-26 02:01:10 +13:00
}