Status bar, WIP

This commit is contained in:
Philipp Heckel 2021-10-26 22:41:19 -04:00
parent 49b3898977
commit 38c8267967
6 changed files with 72 additions and 21 deletions

View file

@ -14,7 +14,10 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.heckel.ntfy.add.AddTopicActivity import io.heckel.ntfy.add.AddTopicActivity
import io.heckel.ntfy.data.Status
import io.heckel.ntfy.data.Topic import io.heckel.ntfy.data.Topic
import io.heckel.ntfy.data.topicShortUrl
import io.heckel.ntfy.data.topicUrl
import io.heckel.ntfy.detail.DetailActivity import io.heckel.ntfy.detail.DetailActivity
import kotlin.random.Random import kotlin.random.Random
@ -74,7 +77,7 @@ class MainActivity : AppCompatActivity() {
intentData?.let { data -> intentData?.let { data ->
val name = data.getStringExtra(TOPIC_NAME) ?: return val name = data.getStringExtra(TOPIC_NAME) ?: return
val baseUrl = data.getStringExtra(TOPIC_BASE_URL) ?: return val baseUrl = data.getStringExtra(TOPIC_BASE_URL) ?: return
val topic = Topic(Random.nextLong(), name, baseUrl) val topic = Topic(Random.nextLong(), name, baseUrl, Status.CONNECTING, 0)
topicsViewModel.add(topic) topicsViewModel.add(topic)
} }
@ -85,7 +88,7 @@ class MainActivity : AppCompatActivity() {
val channelId = getString(R.string.notification_channel_id) val channelId = getString(R.string.notification_channel_id)
val notification = NotificationCompat.Builder(this, channelId) val notification = NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.ntfy) .setSmallIcon(R.drawable.ntfy)
.setContentTitle(n.topic) .setContentTitle(topicShortUrl(n.topic))
.setContentText(n.message) .setContentText(n.message)
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.build() .build()

View file

@ -1,5 +1,6 @@
package io.heckel.ntfy package io.heckel.ntfy
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -7,7 +8,9 @@ import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.heckel.ntfy.data.Status
import io.heckel.ntfy.data.Topic import io.heckel.ntfy.data.Topic
import io.heckel.ntfy.data.topicUrl
class TopicsAdapter(private val onClick: (Topic) -> Unit) : class TopicsAdapter(private val onClick: (Topic) -> Unit) :
ListAdapter<Topic, TopicsAdapter.TopicViewHolder>(TopicDiffCallback) { ListAdapter<Topic, TopicsAdapter.TopicViewHolder>(TopicDiffCallback) {
@ -15,22 +18,32 @@ class TopicsAdapter(private val onClick: (Topic) -> Unit) :
/* ViewHolder for Topic, takes in the inflated view and the onClick behavior. */ /* ViewHolder for Topic, takes in the inflated view and the onClick behavior. */
class TopicViewHolder(itemView: View, val onClick: (Topic) -> Unit) : class TopicViewHolder(itemView: View, val onClick: (Topic) -> Unit) :
RecyclerView.ViewHolder(itemView) { RecyclerView.ViewHolder(itemView) {
private val topicTextView: TextView = itemView.findViewById(R.id.topic_text) private var topic: Topic? = null
private var currentTopic: Topic? = null private val context: Context = itemView.context
private val nameView: TextView = itemView.findViewById(R.id.topic_text)
private val statusView: TextView = itemView.findViewById(R.id.topic_status)
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
currentTopic?.let { topic?.let {
onClick(it) onClick(it)
} }
} }
} }
fun bind(topic: Topic) { fun bind(topic: Topic) {
currentTopic = topic this.topic = topic
val shortBaseUrl = topic.baseUrl.replace("https://", "") // Leave http:// untouched val statusText = when (topic.status) {
val shortName = itemView.context.getString(R.string.topic_short_name_format, shortBaseUrl, topic.name) Status.CONNECTING -> context.getString(R.string.status_connecting)
topicTextView.text = shortName else -> context.getString(R.string.status_subscribed)
}
val statusMessage = if (topic.messages == 1) {
context.getString(R.string.status_text_one, statusText, topic.messages)
} else {
context.getString(R.string.status_text_not_one, statusText, topic.messages)
}
nameView.text = topicUrl(topic)
statusView.text = statusMessage
} }
} }
@ -45,16 +58,15 @@ class TopicsAdapter(private val onClick: (Topic) -> Unit) :
override fun onBindViewHolder(holder: TopicViewHolder, position: Int) { override fun onBindViewHolder(holder: TopicViewHolder, position: Int) {
val topic = getItem(position) val topic = getItem(position)
holder.bind(topic) holder.bind(topic)
} }
} }
object TopicDiffCallback : DiffUtil.ItemCallback<Topic>() { object TopicDiffCallback : DiffUtil.ItemCallback<Topic>() {
override fun areItemsTheSame(oldItem: Topic, newItem: Topic): Boolean { override fun areItemsTheSame(oldItem: Topic, newItem: Topic): Boolean {
return oldItem == newItem return oldItem.id == newItem.id
} }
override fun areContentsTheSame(oldItem: Topic, newItem: Topic): Boolean { override fun areContentsTheSame(oldItem: Topic, newItem: Topic): Boolean {
return oldItem.name == newItem.name return oldItem == newItem
} }
} }

View file

@ -8,7 +8,7 @@ import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.data.Topic import io.heckel.ntfy.data.Topic
import kotlin.collections.List import kotlin.collections.List
data class Notification(val topic: String, val message: String) data class Notification(val topic: Topic, val message: String)
typealias NotificationListener = (notification: Notification) -> Unit typealias NotificationListener = (notification: Notification) -> Unit
class TopicsViewModel(private val repository: Repository) : ViewModel() { class TopicsViewModel(private val repository: Repository) : ViewModel() {

View file

@ -12,14 +12,14 @@ import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
const val READ_TIMEOUT = 60_000 // Keep alive every 30s assumed
class Repository { class Repository {
private val READ_TIMEOUT = 60_000 // Keep alive every 30s assumed
private val topics: MutableLiveData<List<Topic>> = MutableLiveData(mutableListOf()) private val topics: MutableLiveData<List<Topic>> = MutableLiveData(mutableListOf())
private val jobs = mutableMapOf<Long, Job>() private val jobs = mutableMapOf<Long, Job>()
private val gson = GsonBuilder().create() private val gson = GsonBuilder().create()
private var notificationListener: NotificationListener? = null; private var notificationListener: NotificationListener? = null;
/* Adds topic to liveData and posts value. */
fun add(topic: Topic, scope: CoroutineScope) { fun add(topic: Topic, scope: CoroutineScope) {
val currentList = topics.value val currentList = topics.value
if (currentList == null) { if (currentList == null) {
@ -32,7 +32,24 @@ class Repository {
jobs[topic.id] = subscribeTopic(topic, scope) jobs[topic.id] = subscribeTopic(topic, scope)
} }
/* Removes topic from liveData and posts value. */ fun update(topic: Topic) {
val currentList = topics.value
if (currentList == null) {
topics.postValue(listOf(topic))
} else {
val index = currentList.indexOfFirst { it.id == topic.id } // Find index by Topic ID
if (index == -1) {
return // TODO race?
} else {
val updatedList = currentList.toMutableList()
updatedList[index] = topic
println("PHIL updated list:")
println(updatedList)
topics.postValue(updatedList)
}
}
}
fun remove(topic: Topic) { fun remove(topic: Topic) {
val currentList = topics.value val currentList = topics.value
if (currentList != null) { if (currentList != null) {
@ -40,10 +57,9 @@ class Repository {
updatedList.remove(topic) updatedList.remove(topic)
topics.postValue(updatedList) topics.postValue(updatedList)
} }
jobs.remove(topic.id)?.cancel() // Cancel and remove jobs.remove(topic.id)?.cancel() // Cancel coroutine and remove
} }
/* Returns topic given an ID. */
fun get(id: Long): Topic? { fun get(id: Long): Topic? {
topics.value?.let { topics -> topics.value?.let { topics ->
return topics.firstOrNull{ it.id == id} return topics.firstOrNull{ it.id == id}
@ -75,6 +91,7 @@ class Repository {
it.doInput = true it.doInput = true
it.readTimeout = READ_TIMEOUT it.readTimeout = READ_TIMEOUT
} }
update(topic.copy(status = Status.SUBSCRIBED))
try { try {
val input = conn.inputStream.bufferedReader() val input = conn.inputStream.bufferedReader()
while (scope.isActive) { while (scope.isActive) {
@ -86,7 +103,13 @@ class Repository {
val json = gson.fromJson(line, JsonObject::class.java) ?: break // Break on unexpected line val json = gson.fromJson(line, JsonObject::class.java) ?: break // Break on unexpected line
if (!json.isJsonNull && json.has("message")) { if (!json.isJsonNull && json.has("message")) {
val message = json.get("message").asString val message = json.get("message").asString
notificationListener?.let { it(Notification(topic.name, message)) } notificationListener?.let { it(Notification(topic, message)) }
// TODO ugly
val currentTopic = get(topic.id)
if (currentTopic != null) {
update(currentTopic.copy(messages = currentTopic.messages+1))
}
} }
} catch (e: JsonSyntaxException) { } catch (e: JsonSyntaxException) {
break // Break on unexpected line break // Break on unexpected line
@ -97,6 +120,7 @@ class Repository {
} finally { } finally {
conn.disconnect() conn.disconnect()
} }
update(topic.copy(status = Status.CONNECTING))
println("Connection terminated: $url") println("Connection terminated: $url")
} }

View file

@ -1,7 +1,16 @@
package io.heckel.ntfy.data package io.heckel.ntfy.data
enum class Status {
SUBSCRIBED, CONNECTING
}
data class Topic( data class Topic(
val id: Long, // Internal to Repository only val id: Long, // Internal ID, only used in Repository and activities
val name: String, val name: String,
val baseUrl: String, val baseUrl: String,
val status: Status,
val messages: Int
) )
fun topicUrl(t: Topic) = "${t.baseUrl}/${t.name}"
fun topicShortUrl(t: Topic) = topicUrl(t).replace("http://", "").replace("https://", "")

View file

@ -6,8 +6,11 @@
<string name="topic_name_edit_text">Topic Name</string> <string name="topic_name_edit_text">Topic Name</string>
<string name="topic_base_url_edit_text">Service URL</string> <string name="topic_base_url_edit_text">Service URL</string>
<string name="topic_base_url_default_value">https://ntfy.sh</string> <string name="topic_base_url_default_value">https://ntfy.sh</string>
<string name="topic_short_name_format">%1$s/%2$s</string>
<string name="subscribe_button_text">Subscribe</string> <string name="subscribe_button_text">Subscribe</string>
<string name="status_subscribed">Subscribed</string>
<string name="status_connecting">Connecting</string>
<string name="status_text_one">%1$s, %2$d notification</string>
<string name="status_text_not_one">%1$s, %2$d notifications</string>
<string name="fab_content_description">fab</string> <string name="fab_content_description">fab</string>
<string name="remove_topic">Unsubscribe</string> <string name="remove_topic">Unsubscribe</string>