From 38c82679675d44ac5b6d170e1d2d48ef7fc4be12 Mon Sep 17 00:00:00 2001 From: Philipp Heckel <pheckel@datto.com> Date: Tue, 26 Oct 2021 22:41:19 -0400 Subject: [PATCH] Status bar, WIP --- .../main/java/io/heckel/ntfy/MainActivity.kt | 7 ++-- .../main/java/io/heckel/ntfy/TopicsAdapter.kt | 32 +++++++++++------ .../java/io/heckel/ntfy/TopicsViewModel.kt | 2 +- .../java/io/heckel/ntfy/data/Repository.kt | 36 +++++++++++++++---- .../main/java/io/heckel/ntfy/data/Topic.kt | 11 +++++- app/src/main/res/values/strings.xml | 5 ++- 6 files changed, 72 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/io/heckel/ntfy/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/MainActivity.kt index 746b815..2d3c6a1 100644 --- a/app/src/main/java/io/heckel/ntfy/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/MainActivity.kt @@ -14,7 +14,10 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.recyclerview.widget.RecyclerView import io.heckel.ntfy.add.AddTopicActivity +import io.heckel.ntfy.data.Status 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 kotlin.random.Random @@ -74,7 +77,7 @@ class MainActivity : AppCompatActivity() { intentData?.let { data -> val name = data.getStringExtra(TOPIC_NAME) ?: 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) } @@ -85,7 +88,7 @@ class MainActivity : AppCompatActivity() { val channelId = getString(R.string.notification_channel_id) val notification = NotificationCompat.Builder(this, channelId) .setSmallIcon(R.drawable.ntfy) - .setContentTitle(n.topic) + .setContentTitle(topicShortUrl(n.topic)) .setContentText(n.message) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .build() diff --git a/app/src/main/java/io/heckel/ntfy/TopicsAdapter.kt b/app/src/main/java/io/heckel/ntfy/TopicsAdapter.kt index f97080f..dced274 100644 --- a/app/src/main/java/io/heckel/ntfy/TopicsAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/TopicsAdapter.kt @@ -1,5 +1,6 @@ package io.heckel.ntfy +import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -7,7 +8,9 @@ import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import io.heckel.ntfy.data.Status import io.heckel.ntfy.data.Topic +import io.heckel.ntfy.data.topicUrl class TopicsAdapter(private val onClick: (Topic) -> Unit) : 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. */ class TopicViewHolder(itemView: View, val onClick: (Topic) -> Unit) : RecyclerView.ViewHolder(itemView) { - private val topicTextView: TextView = itemView.findViewById(R.id.topic_text) - private var currentTopic: Topic? = null + private var topic: 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 { itemView.setOnClickListener { - currentTopic?.let { + topic?.let { onClick(it) } } } fun bind(topic: Topic) { - currentTopic = topic - val shortBaseUrl = topic.baseUrl.replace("https://", "") // Leave http:// untouched - val shortName = itemView.context.getString(R.string.topic_short_name_format, shortBaseUrl, topic.name) - topicTextView.text = shortName + this.topic = topic + val statusText = when (topic.status) { + Status.CONNECTING -> context.getString(R.string.status_connecting) + 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) { val topic = getItem(position) holder.bind(topic) - } } object TopicDiffCallback : DiffUtil.ItemCallback<Topic>() { override fun areItemsTheSame(oldItem: Topic, newItem: Topic): Boolean { - return oldItem == newItem + return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: Topic, newItem: Topic): Boolean { - return oldItem.name == newItem.name + return oldItem == newItem } } diff --git a/app/src/main/java/io/heckel/ntfy/TopicsViewModel.kt b/app/src/main/java/io/heckel/ntfy/TopicsViewModel.kt index 7cf58ba..9faf507 100644 --- a/app/src/main/java/io/heckel/ntfy/TopicsViewModel.kt +++ b/app/src/main/java/io/heckel/ntfy/TopicsViewModel.kt @@ -8,7 +8,7 @@ import io.heckel.ntfy.data.Repository import io.heckel.ntfy.data.Topic 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 class TopicsViewModel(private val repository: Repository) : ViewModel() { diff --git a/app/src/main/java/io/heckel/ntfy/data/Repository.kt b/app/src/main/java/io/heckel/ntfy/data/Repository.kt index 99b34aa..87bc8e2 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Repository.kt @@ -12,14 +12,14 @@ import java.io.IOException import java.net.HttpURLConnection import java.net.URL +const val READ_TIMEOUT = 60_000 // Keep alive every 30s assumed + class Repository { - private val READ_TIMEOUT = 60_000 // Keep alive every 30s assumed private val topics: MutableLiveData<List<Topic>> = MutableLiveData(mutableListOf()) private val jobs = mutableMapOf<Long, Job>() private val gson = GsonBuilder().create() private var notificationListener: NotificationListener? = null; - /* Adds topic to liveData and posts value. */ fun add(topic: Topic, scope: CoroutineScope) { val currentList = topics.value if (currentList == null) { @@ -32,7 +32,24 @@ class Repository { 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) { val currentList = topics.value if (currentList != null) { @@ -40,10 +57,9 @@ class Repository { updatedList.remove(topic) 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? { topics.value?.let { topics -> return topics.firstOrNull{ it.id == id} @@ -75,6 +91,7 @@ class Repository { it.doInput = true it.readTimeout = READ_TIMEOUT } + update(topic.copy(status = Status.SUBSCRIBED)) try { val input = conn.inputStream.bufferedReader() while (scope.isActive) { @@ -86,7 +103,13 @@ class Repository { val json = gson.fromJson(line, JsonObject::class.java) ?: break // Break on unexpected line if (!json.isJsonNull && json.has("message")) { 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) { break // Break on unexpected line @@ -97,6 +120,7 @@ class Repository { } finally { conn.disconnect() } + update(topic.copy(status = Status.CONNECTING)) println("Connection terminated: $url") } diff --git a/app/src/main/java/io/heckel/ntfy/data/Topic.kt b/app/src/main/java/io/heckel/ntfy/data/Topic.kt index 390c408..558cdfb 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Topic.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Topic.kt @@ -1,7 +1,16 @@ package io.heckel.ntfy.data +enum class Status { + SUBSCRIBED, CONNECTING +} + 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 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://", "") diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bc8db6a..05b351d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,8 +6,11 @@ <string name="topic_name_edit_text">Topic Name</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_short_name_format">%1$s/%2$s</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="remove_topic">Unsubscribe</string>