Do not crash on wrong URL

This commit is contained in:
Philipp Heckel 2021-10-25 21:14:09 -04:00
parent ebe3ab4505
commit 12d194b8c4
6 changed files with 39 additions and 82 deletions

View file

@ -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 Thanks to these projects for allowing me to copy-paste a lot.
[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.

View file

@ -22,8 +22,7 @@ import androidx.lifecycle.MutableLiveData
/* Handles operations on topicsLiveData and holds details about it. */ /* Handles operations on topicsLiveData and holds details about it. */
class DataSource(resources: Resources) { class DataSource(resources: Resources) {
private val initialTopicList: List<Topic> = mutableListOf() private val topicsLiveData: MutableLiveData<List<Topic>> = MutableLiveData(mutableListOf())
private val topicsLiveData = MutableLiveData(initialTopicList)
/* Adds topic to liveData and posts value. */ /* Adds topic to liveData and posts value. */
fun addTopic(topic: Topic) { fun addTopic(topic: Topic) {

View file

@ -11,34 +11,27 @@ import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
data class Event(val name: String = "", val data: JsonObject = JsonObject())
class NtfyApi(context: Context) { class NtfyApi(context: Context) {
private val gson = GsonBuilder().create() private val gson = GsonBuilder().create()
private suspend fun getStreamConnection(url: String): HttpURLConnection = fun createEventsFlow(url: String): Flow<Event> = flow {
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<Event> = flow {
coroutineScope { coroutineScope {
println("111111111111") val conn = getStreamConnection(url)
val conn = getStreamConnection("https://ntfy.sh/_phil/sse")
println("2222222222222") println("2222222222222")
val input = conn.inputStream.bufferedReader() val input = conn.inputStream.bufferedReader()
try { try {
conn.connect() conn.connect()
var event = Event() var event = Event()
println("CCCCCCCCCCCCCCc")
while (isActive) { while (isActive) {
val line = input.readLine() val line = input.readLine()
println("PHIL: " + line) println("PHIL: " + line)
when { when {
line == null -> {
this.cancel(CancellationException("EOF"))
break
}
line.startsWith("event:") -> { line.startsWith("event:") -> {
event = event.copy(name = line.substring(6).trim()) 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
}
}
} }

View file

@ -25,7 +25,6 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.list.TOPIC_ID import io.heckel.ntfy.list.TOPIC_ID
class TopicDetailActivity : AppCompatActivity() { class TopicDetailActivity : AppCompatActivity() {
private val topicDetailViewModel by viewModels<TopicDetailViewModel> { private val topicDetailViewModel by viewModels<TopicDetailViewModel> {
TopicDetailViewModelFactory(this) TopicDetailViewModelFactory(this)
} }

View file

@ -34,6 +34,7 @@ import androidx.recyclerview.widget.RecyclerView
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.add.AddTopicActivity import io.heckel.ntfy.add.AddTopicActivity
import io.heckel.ntfy.add.TOPIC_URL import io.heckel.ntfy.add.TOPIC_URL
import io.heckel.ntfy.data.Event
import io.heckel.ntfy.data.NtfyApi import io.heckel.ntfy.data.NtfyApi
import io.heckel.ntfy.data.Topic import io.heckel.ntfy.data.Topic
import io.heckel.ntfy.detail.TopicDetailActivity import io.heckel.ntfy.detail.TopicDetailActivity
@ -59,7 +60,7 @@ class TopicsListActivity : AppCompatActivity() {
val recyclerView: RecyclerView = findViewById(R.id.recycler_view) val recyclerView: RecyclerView = findViewById(R.id.recycler_view)
recyclerView.adapter = adapter recyclerView.adapter = adapter
topicsListViewModel.topicsLiveData.observe(this) { topicsListViewModel.topics.observe(this) {
it?.let { it?.let {
adapter.submitList(it as MutableList<Topic>) adapter.submitList(it as MutableList<Topic>)
} }
@ -71,8 +72,10 @@ class TopicsListActivity : AppCompatActivity() {
} }
createNotificationChannel() 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) { this.lifecycleScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
handleEvent(event) handleEvent(event)
@ -80,8 +83,7 @@ class TopicsListActivity : AppCompatActivity() {
} }
} }
} }
private fun handleEvent(event: Event) {
private fun handleEvent(event: NtfyApi.Event) {
if (event.data.isJsonNull || !event.data.has("message")) { if (event.data.isJsonNull || !event.data.has("message")) {
return return
} }
@ -117,8 +119,9 @@ class TopicsListActivity : AppCompatActivity() {
/* Inserts topic into viewModel. */ /* Inserts topic into viewModel. */
if (requestCode == newTopicActivityRequestCode && resultCode == Activity.RESULT_OK) { if (requestCode == newTopicActivityRequestCode && resultCode == Activity.RESULT_OK) {
intentData?.let { data -> intentData?.let { data ->
val topicName = data.getStringExtra(TOPIC_URL) val topicUrl = data.getStringExtra(TOPIC_URL) ?: return
topicsListViewModel.insertTopic(topicName) startFlow(topicUrl)
topicsListViewModel.insertTopic(topicUrl)
} }
} }
} }

View file

@ -17,6 +17,7 @@
package io.heckel.ntfy.list package io.heckel.ntfy.list
import android.content.Context import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import io.heckel.ntfy.data.DataSource import io.heckel.ntfy.data.DataSource
@ -24,26 +25,18 @@ import io.heckel.ntfy.data.Topic
import kotlin.random.Random import kotlin.random.Random
class TopicsListViewModel(val dataSource: DataSource) : ViewModel() { class TopicsListViewModel(val dataSource: DataSource) : ViewModel() {
val topics: LiveData<List<Topic>> = dataSource.getTopicList()
val topicsLiveData = dataSource.getTopicList() fun insertTopic(topicUrl: String) {
/* If the name and description are present, create new Topic and add it to the datasource */
fun insertTopic(topicUrl: String?) {
if (topicUrl == null) {
return
}
val newTopic = Topic( val newTopic = Topic(
Random.nextLong(), Random.nextLong(),
topicUrl topicUrl
) )
dataSource.addTopic(newTopic) dataSource.addTopic(newTopic)
} }
} }
class TopicsListViewModelFactory(private val context: Context) : ViewModelProvider.Factory { class TopicsListViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(TopicsListViewModel::class.java)) { if (modelClass.isAssignableFrom(TopicsListViewModel::class.java)) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")