From 12d194b8c41656610778e38449c8c0bef5d85818 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Mon, 25 Oct 2021 21:14:09 -0400 Subject: [PATCH] Do not crash on wrong URL --- README.md | 59 ++++--------------- .../java/io/heckel/ntfy/data/DataSource.kt | 3 +- .../main/java/io/heckel/ntfy/data/NtfyApi.kt | 30 +++++----- .../heckel/ntfy/detail/TopicDetailActivity.kt | 1 - .../io/heckel/ntfy/list/TopicsListActivity.kt | 15 +++-- .../heckel/ntfy/list/TopicsListViewModel.kt | 13 +--- 6 files changed, 39 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index b95fe37..35460f7 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,15 @@ -Android RecyclerView Sample (Kotlin) -==================================== +# ntfy Android App +This is the Android app for [ntfy](https://github.com/binwiederhier/ntfy) ([ntfy.sh](https://ntfy.sh)). +**It is very much work in progress. Also: I'm new to Android development, so I'm still learning.** -This application implements a RecyclerView in Kotlin with ListAdapter, onClickListener -and Headers. If you are looking for a simpler sample, look at the RecyclerViewSimple sample -in the directory. +## ... +... +## License +Made with ❤️ by [Philipp C. Heckel](https://heckel.io), distributed under the [Apache License 2.0](LICENSE). -Introduction ------------- +This app is heavily based on: +* [RecyclerViewKotlin](https://github.com/android/views-widgets-samples/tree/main/RecyclerViewKotlin) (Apache 2.0) +* [Just another Hacker News Android client](https://github.com/manoamaro/another-hacker-news-client) (MIT) -Sample demonstrating the use of [RecyclerView][1] to layout elements with a -[LinearLayoutManager][2]. - -[RecyclerView][1] can display large datasets that can be scrolled -efficiently by recycling a limited number of views. [ListAdapter][3] is used to -efficiently compute diffs when items are added/removed from the list. Click listeners can be -defined when [ViewHolder][4] views are instantiated. - - -[1]: https://developer.android.com/reference/kotlin/androidx/recyclerview/widget/RecyclerView -[2]: https://developer.android.com/reference/androidx/recyclerview/widget/LinearLayoutManager -[3]: https://developer.android.com/reference/androidx/recyclerview/widget/ListAdapter -[4]: https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.ViewHolder - -Pre-requisites --------------- - -- Android SDK 27 -- Android Gradle Plugin 3.0 -- Android Support Repository - -Screenshots -------------- - -![image](https://user-images.githubusercontent.com/46006059/98028846-8b6df700-1dc3-11eb-9f0b-ad93569be189.png) - -Getting Started ---------------- - -To build this project, use "Import Project" in Android Studio. - -Support -------- - -- Stack Overflow: http://stackoverflow.com/questions/tagged/android - -If you've found an error in this sample, please file an issue: -https://github.com/android/views-widgets - -Patches are encouraged, and may be submitted by forking this project and -submitting a pull request through GitHub. Please see CONTRIBUTING.md for more details. \ No newline at end of file +Thanks to these projects for allowing me to copy-paste a lot. diff --git a/app/src/main/java/io/heckel/ntfy/data/DataSource.kt b/app/src/main/java/io/heckel/ntfy/data/DataSource.kt index 7177f2b..f507e2f 100644 --- a/app/src/main/java/io/heckel/ntfy/data/DataSource.kt +++ b/app/src/main/java/io/heckel/ntfy/data/DataSource.kt @@ -22,8 +22,7 @@ import androidx.lifecycle.MutableLiveData /* Handles operations on topicsLiveData and holds details about it. */ class DataSource(resources: Resources) { - private val initialTopicList: List = mutableListOf() - private val topicsLiveData = MutableLiveData(initialTopicList) + private val topicsLiveData: MutableLiveData> = MutableLiveData(mutableListOf()) /* Adds topic to liveData and posts value. */ fun addTopic(topic: Topic) { diff --git a/app/src/main/java/io/heckel/ntfy/data/NtfyApi.kt b/app/src/main/java/io/heckel/ntfy/data/NtfyApi.kt index 36568c6..81c99bd 100644 --- a/app/src/main/java/io/heckel/ntfy/data/NtfyApi.kt +++ b/app/src/main/java/io/heckel/ntfy/data/NtfyApi.kt @@ -11,34 +11,27 @@ import java.io.IOException import java.net.HttpURLConnection import java.net.URL +data class Event(val name: String = "", val data: JsonObject = JsonObject()) + class NtfyApi(context: Context) { private val gson = GsonBuilder().create() - private suspend fun getStreamConnection(url: String): HttpURLConnection = - withContext(Dispatchers.IO) { - return@withContext (URL(url).openConnection() as HttpURLConnection).also { - it.setRequestProperty("Accept", "text/event-stream") - it.doInput = true - } - } - - data class Event(val name: String = "", val data: JsonObject = JsonObject()) - - fun getEventsFlow(): Flow = flow { + fun createEventsFlow(url: String): Flow = flow { coroutineScope { - println("111111111111") - - val conn = getStreamConnection("https://ntfy.sh/_phil/sse") + val conn = getStreamConnection(url) println("2222222222222") val input = conn.inputStream.bufferedReader() try { conn.connect() var event = Event() - println("CCCCCCCCCCCCCCc") while (isActive) { val line = input.readLine() println("PHIL: " + line) when { + line == null -> { + this.cancel(CancellationException("EOF")) + break + } line.startsWith("event:") -> { event = event.copy(name = line.substring(6).trim()) } @@ -65,4 +58,11 @@ class NtfyApi(context: Context) { } } } + + private suspend fun getStreamConnection(url: String): HttpURLConnection = + withContext(Dispatchers.IO) { + return@withContext (URL(url).openConnection() as HttpURLConnection).also { + it.doInput = true + } + } } diff --git a/app/src/main/java/io/heckel/ntfy/detail/TopicDetailActivity.kt b/app/src/main/java/io/heckel/ntfy/detail/TopicDetailActivity.kt index 122fff3..e31a523 100644 --- a/app/src/main/java/io/heckel/ntfy/detail/TopicDetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/detail/TopicDetailActivity.kt @@ -25,7 +25,6 @@ import io.heckel.ntfy.R import io.heckel.ntfy.list.TOPIC_ID class TopicDetailActivity : AppCompatActivity() { - private val topicDetailViewModel by viewModels { TopicDetailViewModelFactory(this) } diff --git a/app/src/main/java/io/heckel/ntfy/list/TopicsListActivity.kt b/app/src/main/java/io/heckel/ntfy/list/TopicsListActivity.kt index 9113c56..6712676 100644 --- a/app/src/main/java/io/heckel/ntfy/list/TopicsListActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/list/TopicsListActivity.kt @@ -34,6 +34,7 @@ import androidx.recyclerview.widget.RecyclerView import io.heckel.ntfy.R import io.heckel.ntfy.add.AddTopicActivity import io.heckel.ntfy.add.TOPIC_URL +import io.heckel.ntfy.data.Event import io.heckel.ntfy.data.NtfyApi import io.heckel.ntfy.data.Topic import io.heckel.ntfy.detail.TopicDetailActivity @@ -59,7 +60,7 @@ class TopicsListActivity : AppCompatActivity() { val recyclerView: RecyclerView = findViewById(R.id.recycler_view) recyclerView.adapter = adapter - topicsListViewModel.topicsLiveData.observe(this) { + topicsListViewModel.topics.observe(this) { it?.let { adapter.submitList(it as MutableList) } @@ -71,8 +72,10 @@ class TopicsListActivity : AppCompatActivity() { } createNotificationChannel() + } - api.getEventsFlow().asLiveData(Dispatchers.IO).observe(this) { event -> + private fun startFlow(url: String) { + api.createEventsFlow(url).asLiveData(Dispatchers.IO).observe(this) { event -> this.lifecycleScope.launch(Dispatchers.Main) { withContext(Dispatchers.IO) { handleEvent(event) @@ -80,8 +83,7 @@ class TopicsListActivity : AppCompatActivity() { } } } - - private fun handleEvent(event: NtfyApi.Event) { + private fun handleEvent(event: Event) { if (event.data.isJsonNull || !event.data.has("message")) { return } @@ -117,8 +119,9 @@ class TopicsListActivity : AppCompatActivity() { /* Inserts topic into viewModel. */ if (requestCode == newTopicActivityRequestCode && resultCode == Activity.RESULT_OK) { intentData?.let { data -> - val topicName = data.getStringExtra(TOPIC_URL) - topicsListViewModel.insertTopic(topicName) + val topicUrl = data.getStringExtra(TOPIC_URL) ?: return + startFlow(topicUrl) + topicsListViewModel.insertTopic(topicUrl) } } } diff --git a/app/src/main/java/io/heckel/ntfy/list/TopicsListViewModel.kt b/app/src/main/java/io/heckel/ntfy/list/TopicsListViewModel.kt index c53b3de..1ce392c 100644 --- a/app/src/main/java/io/heckel/ntfy/list/TopicsListViewModel.kt +++ b/app/src/main/java/io/heckel/ntfy/list/TopicsListViewModel.kt @@ -17,6 +17,7 @@ package io.heckel.ntfy.list import android.content.Context +import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import io.heckel.ntfy.data.DataSource @@ -24,26 +25,18 @@ import io.heckel.ntfy.data.Topic import kotlin.random.Random class TopicsListViewModel(val dataSource: DataSource) : ViewModel() { + val topics: LiveData> = dataSource.getTopicList() - val topicsLiveData = dataSource.getTopicList() - - /* If the name and description are present, create new Topic and add it to the datasource */ - fun insertTopic(topicUrl: String?) { - if (topicUrl == null) { - return - } - + fun insertTopic(topicUrl: String) { val newTopic = Topic( Random.nextLong(), topicUrl ) - dataSource.addTopic(newTopic) } } class TopicsListViewModelFactory(private val context: Context) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { if (modelClass.isAssignableFrom(TopicsListViewModel::class.java)) { @Suppress("UNCHECKED_CAST")