Switch to /json endpoint

This commit is contained in:
Philipp Heckel 2021-10-26 13:46:49 -04:00
parent 0c26703c78
commit 3751366c19
7 changed files with 55 additions and 115 deletions

View file

@ -21,8 +21,8 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.widget.Button import android.widget.Button
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import io.heckel.ntfy.R
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import io.heckel.ntfy.R
const val TOPIC_URL = "url" const val TOPIC_URL = "url"

View file

@ -1,18 +0,0 @@
package io.heckel.ntfy.data
import android.content.Context
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
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) {
}

View file

@ -36,11 +36,10 @@ import com.google.gson.JsonSyntaxException
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.Topic import io.heckel.ntfy.data.Topic
import io.heckel.ntfy.detail.TopicDetailActivity import io.heckel.ntfy.detail.TopicDetailActivity
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.BufferedReader
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
@ -49,7 +48,7 @@ import kotlin.random.Random
const val TOPIC_ID = "topic id" const val TOPIC_ID = "topic id"
class TopicsListActivity : AppCompatActivity() { class TopicsListActivity : AppCompatActivity() {
private val api = NtfyApi(this) private val gson = GsonBuilder().create()
private val jobs = mutableMapOf<Long, Job>() private val jobs = mutableMapOf<Long, Job>()
private val newTopicActivityRequestCode = 1 private val newTopicActivityRequestCode = 1
private val topicsListViewModel by viewModels<TopicsListViewModel> { private val topicsListViewModel by viewModels<TopicsListViewModel> {
@ -101,12 +100,60 @@ class TopicsListActivity : AppCompatActivity() {
val topicUrl = data.getStringExtra(TOPIC_URL) ?: return val topicUrl = data.getStringExtra(TOPIC_URL) ?: return
val topic = Topic(topicId, topicUrl) val topic = Topic(topicId, topicUrl)
jobs[topicId] = startListening(topicUrl) jobs[topicId] = subscribeTopic(topicUrl)
topicsListViewModel.add(topic) topicsListViewModel.add(topic)
} }
} }
} }
private fun subscribeTopic(url: String): Job {
return this.lifecycleScope.launch(Dispatchers.IO) {
while (isActive) {
openURL(this, url)
delay(5000) // TODO exponential back-off
}
}
}
private fun openURL(scope: CoroutineScope, url: String) {
println("Connecting to $url ...")
val conn = (URL(url).openConnection() as HttpURLConnection).also {
it.doInput = true
}
try {
val input = conn.inputStream.bufferedReader()
while (scope.isActive) {
val line = input.readLine() ?: break // Break if null!
try {
displayNotification(gson.fromJson(line, JsonObject::class.java))
} catch (e: JsonSyntaxException) {
// Ignore invalid JSON
}
}
} catch (e: IOException) {
println("PHIL: " + e.message)
} finally {
conn.disconnect()
}
println("Connection terminated: $url")
}
private fun displayNotification(json: JsonObject) {
if (json.isJsonNull || !json.has("message")) {
return
}
val channelId = getString(R.string.notification_channel_id)
val notification = NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.ntfy)
.setContentTitle("ntfy")
.setContentText(json.get("message").asString)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.build()
with(NotificationManagerCompat.from(this)) {
notify(Random.nextInt(), notification)
}
}
private fun createNotificationChannel() { private fun createNotificationChannel() {
// Create the NotificationChannel, but only on API 26+ because // Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library // the NotificationChannel class is new and not in the support library
@ -124,71 +171,4 @@ class TopicsListActivity : AppCompatActivity() {
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
} }
} }
private val gson = GsonBuilder().create()
fun startListening(url: String): Job {
return this.lifecycleScope.launch(Dispatchers.IO) {
while (isActive) {
println("connecting ...")
val conn = (URL(url).openConnection() as HttpURLConnection).also {
it.doInput = true
}
val input = conn.inputStream.bufferedReader()
try {
conn.connect()
var event = Event()
while (isActive) {
val line = input.readLine()
println("PHIL: " + line)
when {
line == null -> {
println("line is null")
break
}
line.startsWith("event:") -> {
event = event.copy(name = line.substring(6).trim())
}
line.startsWith("data:") -> {
val data = line.substring(5).trim()
try {
event = event.copy(data = gson.fromJson(data, JsonObject::class.java))
} catch (e: JsonSyntaxException) {
// Nothing
}
}
line.isEmpty() -> {
handleEvent(event)
event = Event()
}
}
}
} catch (e: IOException) {
println("PHIL: " + e.message)
} finally {
conn.disconnect()
input.close()
}
println("connection died")
delay(5000)
}
}
}
private fun handleEvent(event: Event) {
if (event.data.isJsonNull || !event.data.has("message")) {
return
}
println("PHIL EVENT: " + event.data)
val channelId = getString(R.string.notification_channel_id)
val notification = NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.ntfy)
.setContentTitle("ntfy")
.setContentText(event.data.get("message").asString)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.build()
with(NotificationManagerCompat.from(this)) {
notify(Random.nextInt(), notification)
}
}
} }

View file

@ -22,7 +22,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import io.heckel.ntfy.data.DataSource import io.heckel.ntfy.data.DataSource
import io.heckel.ntfy.data.Topic import io.heckel.ntfy.data.Topic
import kotlin.random.Random
class TopicsListViewModel(val dataSource: DataSource) : ViewModel() { class TopicsListViewModel(val dataSource: DataSource) : ViewModel() {
val topics: LiveData<List<Topic>> = dataSource.getTopicList() val topics: LiveData<List<Topic>> = dataSource.getTopicList()

View file

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

View file

@ -13,10 +13,9 @@
--> -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="match_parent"
android:layout_height="match_parent" android:orientation="vertical">
android:orientation="vertical">
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -12,7 +12,6 @@
limitations under the License. limitations under the License.
--> -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="80dp" android:layout_height="80dp"
android:orientation="vertical"> android:orientation="vertical">