Do not crash on wrong URL
This commit is contained in:
parent
ebe3ab4505
commit
12d194b8c4
6 changed files with 39 additions and 82 deletions
59
README.md
59
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.
|
||||
Thanks to these projects for allowing me to copy-paste a lot.
|
||||
|
|
|
@ -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<Topic> = mutableListOf()
|
||||
private val topicsLiveData = MutableLiveData(initialTopicList)
|
||||
private val topicsLiveData: MutableLiveData<List<Topic>> = MutableLiveData(mutableListOf())
|
||||
|
||||
/* Adds topic to liveData and posts value. */
|
||||
fun addTopic(topic: Topic) {
|
||||
|
|
|
@ -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<Event> = flow {
|
||||
fun createEventsFlow(url: String): Flow<Event> = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ import io.heckel.ntfy.R
|
|||
import io.heckel.ntfy.list.TOPIC_ID
|
||||
|
||||
class TopicDetailActivity : AppCompatActivity() {
|
||||
|
||||
private val topicDetailViewModel by viewModels<TopicDetailViewModel> {
|
||||
TopicDetailViewModelFactory(this)
|
||||
}
|
||||
|
|
|
@ -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<Topic>)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<List<Topic>> = 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 <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(TopicsListViewModel::class.java)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
|
Loading…
Reference in a new issue