Loading app/src/main/java/io/heckel/ntfy/MainActivity.kt +5 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) } Loading @@ -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() Loading app/src/main/java/io/heckel/ntfy/TopicsAdapter.kt +22 −10 Original line number Diff line number Diff line package io.heckel.ntfy import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup Loading @@ -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) { Loading @@ -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 } } Loading @@ -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 } } app/src/main/java/io/heckel/ntfy/TopicsViewModel.kt +1 −1 Original line number Diff line number Diff line Loading @@ -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() { Loading app/src/main/java/io/heckel/ntfy/data/Repository.kt +30 −6 Original line number Diff line number Diff line Loading @@ -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) { Loading @@ -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) { Loading @@ -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} Loading Loading @@ -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) { Loading @@ -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 Loading @@ -97,6 +120,7 @@ class Repository { } finally { conn.disconnect() } update(topic.copy(status = Status.CONNECTING)) println("Connection terminated: $url") } Loading app/src/main/java/io/heckel/ntfy/data/Topic.kt +10 −1 Original line number Diff line number Diff line 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://", "") Loading
app/src/main/java/io/heckel/ntfy/MainActivity.kt +5 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) } Loading @@ -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() Loading
app/src/main/java/io/heckel/ntfy/TopicsAdapter.kt +22 −10 Original line number Diff line number Diff line package io.heckel.ntfy import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup Loading @@ -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) { Loading @@ -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 } } Loading @@ -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 } }
app/src/main/java/io/heckel/ntfy/TopicsViewModel.kt +1 −1 Original line number Diff line number Diff line Loading @@ -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() { Loading
app/src/main/java/io/heckel/ntfy/data/Repository.kt +30 −6 Original line number Diff line number Diff line Loading @@ -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) { Loading @@ -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) { Loading @@ -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} Loading Loading @@ -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) { Loading @@ -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 Loading @@ -97,6 +120,7 @@ class Repository { } finally { conn.disconnect() } update(topic.copy(status = Status.CONNECTING)) println("Connection terminated: $url") } Loading
app/src/main/java/io/heckel/ntfy/data/Topic.kt +10 −1 Original line number Diff line number Diff line 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://", "")