Loading app/src/main/java/io/heckel/ntfy/MainActivity.kt +6 −22 Original line number Diff line number Diff line /* * 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. */ package io.heckel.ntfy import android.app.Activity Loading @@ -32,11 +16,11 @@ import androidx.recyclerview.widget.RecyclerView import io.heckel.ntfy.add.AddTopicActivity import io.heckel.ntfy.data.Topic import io.heckel.ntfy.detail.DetailActivity import io.heckel.ntfy.list.* import kotlin.random.Random const val TOPIC_ID = "topic id" const val TOPIC_URL = "url" const val TOPIC_ID = "topic_id" const val TOPIC_NAME = "topic_name" const val TOPIC_BASE_URL = "base_url" class MainActivity : AppCompatActivity() { private val newTopicActivityRequestCode = 1 Loading Loading @@ -88,9 +72,9 @@ class MainActivity : AppCompatActivity() { if (requestCode == newTopicActivityRequestCode && resultCode == Activity.RESULT_OK) { intentData?.let { data -> val topicId = Random.nextLong() val topicUrl = data.getStringExtra(TOPIC_URL) ?: return val topic = Topic(topicId, topicUrl) val name = data.getStringExtra(TOPIC_NAME) ?: return val baseUrl = data.getStringExtra(TOPIC_BASE_URL) ?: return val topic = Topic(Random.nextLong(), name, baseUrl) topicsViewModel.add(topic) } Loading app/src/main/java/io/heckel/ntfy/TopicsAdapter.kt +5 −21 Original line number Diff line number Diff line /* * 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. */ package io.heckel.ntfy.list package io.heckel.ntfy import android.view.LayoutInflater import android.view.View Loading @@ -23,7 +7,6 @@ import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import io.heckel.ntfy.R import io.heckel.ntfy.data.Topic class TopicsAdapter(private val onClick: (Topic) -> Unit) : Loading @@ -43,10 +26,11 @@ class TopicsAdapter(private val onClick: (Topic) -> Unit) : } } /* Bind topic name and image. */ fun bind(topic: Topic) { currentTopic = topic topicTextView.text = topic.url 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 } } Loading @@ -71,6 +55,6 @@ object TopicDiffCallback : DiffUtil.ItemCallback<Topic>() { } override fun areContentsTheSame(oldItem: Topic, newItem: Topic): Boolean { return oldItem.id == newItem.id return oldItem.name == newItem.name } } app/src/main/java/io/heckel/ntfy/TopicsViewModel.kt +3 −4 Original line number Diff line number Diff line Loading @@ -4,15 +4,14 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import io.heckel.ntfy.data.TopicsRepository import io.heckel.ntfy.data.Repository import io.heckel.ntfy.data.Topic import kotlinx.coroutines.* import kotlin.collections.List data class Notification(val topic: String, val message: String) typealias NotificationListener = (notification: Notification) -> Unit class TopicsViewModel(private val repository: TopicsRepository) : ViewModel() { class TopicsViewModel(private val repository: Repository) : ViewModel() { fun add(topic: Topic) { repository.add(topic, viewModelScope) } Loading @@ -39,7 +38,7 @@ class TopicsViewModelFactory() : ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>) = with(modelClass){ when { isAssignableFrom(TopicsViewModel::class.java) -> TopicsViewModel(TopicsRepository.getInstance()) as T isAssignableFrom(TopicsViewModel::class.java) -> TopicsViewModel(Repository.getInstance()) as T else -> throw IllegalArgumentException("Unknown viewModel class $modelClass") } } Loading app/src/main/java/io/heckel/ntfy/add/AddActivity.kt +12 −23 Original line number Diff line number Diff line /* * 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. */ package io.heckel.ntfy.add import android.app.Activity Loading @@ -23,10 +7,12 @@ import android.widget.Button import androidx.appcompat.app.AppCompatActivity import com.google.android.material.textfield.TextInputEditText import io.heckel.ntfy.R import io.heckel.ntfy.TOPIC_URL import io.heckel.ntfy.TOPIC_BASE_URL import io.heckel.ntfy.TOPIC_NAME class AddTopicActivity : AppCompatActivity() { private lateinit var addTopicUrl: TextInputEditText private lateinit var topicName: TextInputEditText private lateinit var baseUrl: TextInputEditText override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Loading @@ -35,8 +21,9 @@ class AddTopicActivity : AppCompatActivity() { findViewById<Button>(R.id.subscribe_button).setOnClickListener { addTopic() } addTopicUrl = findViewById(R.id.add_topic_url) addTopicUrl.setText("https://ntfy.sh/") topicName = findViewById(R.id.add_topic_name) baseUrl = findViewById(R.id.add_topic_base_url) baseUrl.setText(R.string.topic_base_url_default_value) } /* The onClick action for the done button. Closes the activity and returns the new topic name Loading @@ -46,11 +33,13 @@ class AddTopicActivity : AppCompatActivity() { private fun addTopic() { val resultIntent = Intent() if (addTopicUrl.text.isNullOrEmpty()) { // TODO don't allow this if (baseUrl.text.isNullOrEmpty()) { setResult(Activity.RESULT_CANCELED, resultIntent) } else { val url = addTopicUrl.text.toString() resultIntent.putExtra(TOPIC_URL, url) resultIntent.putExtra(TOPIC_NAME, topicName.text.toString()) resultIntent.putExtra(TOPIC_BASE_URL, baseUrl.text.toString()) setResult(Activity.RESULT_OK, resultIntent) } finish() Loading app/src/main/java/io/heckel/ntfy/data/TopicsRepository.kt→app/src/main/java/io/heckel/ntfy/data/Repository.kt +18 −13 Original line number Diff line number Diff line Loading @@ -12,8 +12,8 @@ import java.io.IOException import java.net.HttpURLConnection import java.net.URL /* Handles operations on topicsLiveData and holds details about it. */ class TopicsRepository { 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() Loading Loading @@ -62,33 +62,38 @@ class TopicsRepository { private fun subscribeTopic(topic: Topic, scope: CoroutineScope): Job { return scope.launch(Dispatchers.IO) { while (isActive) { openURL(this, topic.url, topic.url) // TODO openConnection(this, topic) delay(5000) // TODO exponential back-off } } } private fun openURL(scope: CoroutineScope, topic: String, url: String) { private fun openConnection(scope: CoroutineScope, topic: Topic) { val url = "${topic.baseUrl}/${topic.name}/json" println("Connecting to $url ...") val conn = (URL(url).openConnection() as HttpURLConnection).also { it.doInput = true it.readTimeout = READ_TIMEOUT } try { val input = conn.inputStream.bufferedReader() while (scope.isActive) { val line = input.readLine() ?: break // Exit if null val line = input.readLine() ?: break // Break if EOF is reached, i.e. readLine is null if (!scope.isActive) { break // Break if scope is not active anymore; readLine blocks for a while, so we want to be sure } try { val json = gson.fromJson(line, JsonObject::class.java) ?: break // Exit if null 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(url, message)) } notificationListener?.let { it(Notification(topic.name, message)) } } } catch (e: JsonSyntaxException) { // Ignore invalid JSON break // Break on unexpected line } } } catch (e: IOException) { println("PHIL: " + e.message) println("Connection error: " + e.message) } finally { conn.disconnect() } Loading @@ -96,11 +101,11 @@ class TopicsRepository { } companion object { private var instance: TopicsRepository? = null private var instance: Repository? = null fun getInstance(): TopicsRepository { return synchronized(TopicsRepository::class) { val newInstance = instance ?: TopicsRepository() fun getInstance(): Repository { return synchronized(Repository::class) { val newInstance = instance ?: Repository() instance = newInstance newInstance } Loading Loading
app/src/main/java/io/heckel/ntfy/MainActivity.kt +6 −22 Original line number Diff line number Diff line /* * 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. */ package io.heckel.ntfy import android.app.Activity Loading @@ -32,11 +16,11 @@ import androidx.recyclerview.widget.RecyclerView import io.heckel.ntfy.add.AddTopicActivity import io.heckel.ntfy.data.Topic import io.heckel.ntfy.detail.DetailActivity import io.heckel.ntfy.list.* import kotlin.random.Random const val TOPIC_ID = "topic id" const val TOPIC_URL = "url" const val TOPIC_ID = "topic_id" const val TOPIC_NAME = "topic_name" const val TOPIC_BASE_URL = "base_url" class MainActivity : AppCompatActivity() { private val newTopicActivityRequestCode = 1 Loading Loading @@ -88,9 +72,9 @@ class MainActivity : AppCompatActivity() { if (requestCode == newTopicActivityRequestCode && resultCode == Activity.RESULT_OK) { intentData?.let { data -> val topicId = Random.nextLong() val topicUrl = data.getStringExtra(TOPIC_URL) ?: return val topic = Topic(topicId, topicUrl) val name = data.getStringExtra(TOPIC_NAME) ?: return val baseUrl = data.getStringExtra(TOPIC_BASE_URL) ?: return val topic = Topic(Random.nextLong(), name, baseUrl) topicsViewModel.add(topic) } Loading
app/src/main/java/io/heckel/ntfy/TopicsAdapter.kt +5 −21 Original line number Diff line number Diff line /* * 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. */ package io.heckel.ntfy.list package io.heckel.ntfy import android.view.LayoutInflater import android.view.View Loading @@ -23,7 +7,6 @@ import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import io.heckel.ntfy.R import io.heckel.ntfy.data.Topic class TopicsAdapter(private val onClick: (Topic) -> Unit) : Loading @@ -43,10 +26,11 @@ class TopicsAdapter(private val onClick: (Topic) -> Unit) : } } /* Bind topic name and image. */ fun bind(topic: Topic) { currentTopic = topic topicTextView.text = topic.url 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 } } Loading @@ -71,6 +55,6 @@ object TopicDiffCallback : DiffUtil.ItemCallback<Topic>() { } override fun areContentsTheSame(oldItem: Topic, newItem: Topic): Boolean { return oldItem.id == newItem.id return oldItem.name == newItem.name } }
app/src/main/java/io/heckel/ntfy/TopicsViewModel.kt +3 −4 Original line number Diff line number Diff line Loading @@ -4,15 +4,14 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import io.heckel.ntfy.data.TopicsRepository import io.heckel.ntfy.data.Repository import io.heckel.ntfy.data.Topic import kotlinx.coroutines.* import kotlin.collections.List data class Notification(val topic: String, val message: String) typealias NotificationListener = (notification: Notification) -> Unit class TopicsViewModel(private val repository: TopicsRepository) : ViewModel() { class TopicsViewModel(private val repository: Repository) : ViewModel() { fun add(topic: Topic) { repository.add(topic, viewModelScope) } Loading @@ -39,7 +38,7 @@ class TopicsViewModelFactory() : ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>) = with(modelClass){ when { isAssignableFrom(TopicsViewModel::class.java) -> TopicsViewModel(TopicsRepository.getInstance()) as T isAssignableFrom(TopicsViewModel::class.java) -> TopicsViewModel(Repository.getInstance()) as T else -> throw IllegalArgumentException("Unknown viewModel class $modelClass") } } Loading
app/src/main/java/io/heckel/ntfy/add/AddActivity.kt +12 −23 Original line number Diff line number Diff line /* * 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. */ package io.heckel.ntfy.add import android.app.Activity Loading @@ -23,10 +7,12 @@ import android.widget.Button import androidx.appcompat.app.AppCompatActivity import com.google.android.material.textfield.TextInputEditText import io.heckel.ntfy.R import io.heckel.ntfy.TOPIC_URL import io.heckel.ntfy.TOPIC_BASE_URL import io.heckel.ntfy.TOPIC_NAME class AddTopicActivity : AppCompatActivity() { private lateinit var addTopicUrl: TextInputEditText private lateinit var topicName: TextInputEditText private lateinit var baseUrl: TextInputEditText override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Loading @@ -35,8 +21,9 @@ class AddTopicActivity : AppCompatActivity() { findViewById<Button>(R.id.subscribe_button).setOnClickListener { addTopic() } addTopicUrl = findViewById(R.id.add_topic_url) addTopicUrl.setText("https://ntfy.sh/") topicName = findViewById(R.id.add_topic_name) baseUrl = findViewById(R.id.add_topic_base_url) baseUrl.setText(R.string.topic_base_url_default_value) } /* The onClick action for the done button. Closes the activity and returns the new topic name Loading @@ -46,11 +33,13 @@ class AddTopicActivity : AppCompatActivity() { private fun addTopic() { val resultIntent = Intent() if (addTopicUrl.text.isNullOrEmpty()) { // TODO don't allow this if (baseUrl.text.isNullOrEmpty()) { setResult(Activity.RESULT_CANCELED, resultIntent) } else { val url = addTopicUrl.text.toString() resultIntent.putExtra(TOPIC_URL, url) resultIntent.putExtra(TOPIC_NAME, topicName.text.toString()) resultIntent.putExtra(TOPIC_BASE_URL, baseUrl.text.toString()) setResult(Activity.RESULT_OK, resultIntent) } finish() Loading
app/src/main/java/io/heckel/ntfy/data/TopicsRepository.kt→app/src/main/java/io/heckel/ntfy/data/Repository.kt +18 −13 Original line number Diff line number Diff line Loading @@ -12,8 +12,8 @@ import java.io.IOException import java.net.HttpURLConnection import java.net.URL /* Handles operations on topicsLiveData and holds details about it. */ class TopicsRepository { 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() Loading Loading @@ -62,33 +62,38 @@ class TopicsRepository { private fun subscribeTopic(topic: Topic, scope: CoroutineScope): Job { return scope.launch(Dispatchers.IO) { while (isActive) { openURL(this, topic.url, topic.url) // TODO openConnection(this, topic) delay(5000) // TODO exponential back-off } } } private fun openURL(scope: CoroutineScope, topic: String, url: String) { private fun openConnection(scope: CoroutineScope, topic: Topic) { val url = "${topic.baseUrl}/${topic.name}/json" println("Connecting to $url ...") val conn = (URL(url).openConnection() as HttpURLConnection).also { it.doInput = true it.readTimeout = READ_TIMEOUT } try { val input = conn.inputStream.bufferedReader() while (scope.isActive) { val line = input.readLine() ?: break // Exit if null val line = input.readLine() ?: break // Break if EOF is reached, i.e. readLine is null if (!scope.isActive) { break // Break if scope is not active anymore; readLine blocks for a while, so we want to be sure } try { val json = gson.fromJson(line, JsonObject::class.java) ?: break // Exit if null 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(url, message)) } notificationListener?.let { it(Notification(topic.name, message)) } } } catch (e: JsonSyntaxException) { // Ignore invalid JSON break // Break on unexpected line } } } catch (e: IOException) { println("PHIL: " + e.message) println("Connection error: " + e.message) } finally { conn.disconnect() } Loading @@ -96,11 +101,11 @@ class TopicsRepository { } companion object { private var instance: TopicsRepository? = null private var instance: Repository? = null fun getInstance(): TopicsRepository { return synchronized(TopicsRepository::class) { val newInstance = instance ?: TopicsRepository() fun getInstance(): Repository { return synchronized(Repository::class) { val newInstance = instance ?: Repository() instance = newInstance newInstance } Loading