From 638c8f093a5918c620abac395d78ba0bccd4d729 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 26 Oct 2021 20:34:09 -0400 Subject: [PATCH] Singleton repository class --- .../main/java/io/heckel/ntfy/MainActivity.kt | 2 +- .../java/io/heckel/ntfy/TopicsViewModel.kt | 112 ++++-------------- .../java/io/heckel/ntfy/data/DataSource.kt | 72 ----------- .../io/heckel/ntfy/data/TopicsRepository.kt | 109 +++++++++++++++++ .../io/heckel/ntfy/detail/DetailActivity.kt | 6 +- 5 files changed, 134 insertions(+), 167 deletions(-) delete mode 100644 app/src/main/java/io/heckel/ntfy/data/DataSource.kt create mode 100644 app/src/main/java/io/heckel/ntfy/data/TopicsRepository.kt diff --git a/app/src/main/java/io/heckel/ntfy/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/MainActivity.kt index dcbd2b0..96110b2 100644 --- a/app/src/main/java/io/heckel/ntfy/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/MainActivity.kt @@ -41,7 +41,7 @@ const val TOPIC_URL = "url" class MainActivity : AppCompatActivity() { private val newTopicActivityRequestCode = 1 private val topicsViewModel by viewModels { - TopicsViewModelFactory(this) + TopicsViewModelFactory() } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/io/heckel/ntfy/TopicsViewModel.kt b/app/src/main/java/io/heckel/ntfy/TopicsViewModel.kt index 46b3976..b58930e 100644 --- a/app/src/main/java/io/heckel/ntfy/TopicsViewModel.kt +++ b/app/src/main/java/io/heckel/ntfy/TopicsViewModel.kt @@ -1,116 +1,46 @@ -/* - * 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 -package io.heckel.ntfy.list - -import android.content.Context -import androidx.lifecycle.* -import com.google.gson.GsonBuilder -import com.google.gson.JsonObject -import com.google.gson.JsonSyntaxException -import io.heckel.ntfy.data.DataSource +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.Topic 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 +import kotlin.collections.List data class Notification(val topic: String, val message: String) typealias NotificationListener = (notification: Notification) -> Unit -class TopicsViewModel(val datasource: DataSource) : ViewModel() { - private val gson = GsonBuilder().create() - private val jobs = mutableMapOf() - private var notificationListener: NotificationListener? = null; - +class TopicsViewModel(private val repository: TopicsRepository) : ViewModel() { fun add(topic: Topic) { - println("Adding topic $topic $this") - datasource.add(topic) - jobs[topic.id] = subscribeTopic(topic.url) + repository.add(topic, viewModelScope) } fun get(id: Long) : Topic? { - return datasource.get(id) + return repository.get(id) } fun list(): LiveData> { - return datasource.list() + return repository.list() } fun remove(topic: Topic) { - println("Removing topic $topic $this") - jobs[topic.id]?.cancel() - println("${jobs[topic.id]}") - - jobs.remove(topic.id)?.cancel() // Cancel and remove - println("${jobs[topic.id]}") - datasource.remove(topic) + repository.remove(topic) } fun setNotificationListener(listener: NotificationListener) { - notificationListener = listener - } - - private fun subscribeTopic(url: String): Job { - return viewModelScope.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 // Exit if null - try { - val json = gson.fromJson(line, JsonObject::class.java) ?: break // Exit if null - if (!json.isJsonNull && json.has("message")) { - val message = json.get("message").asString - notificationListener?.let { it(Notification(url, message)) } - } - } catch (e: JsonSyntaxException) { - // Ignore invalid JSON - } - } - } catch (e: IOException) { - println("PHIL: " + e.message) - } finally { - conn.disconnect() - } - println("Connection terminated: $url") + repository.setNotificationListener(listener) } } -class TopicsViewModelFactory(private val context: Context) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(TopicsViewModel::class.java)) { - @Suppress("UNCHECKED_CAST") - return TopicsViewModel( - datasource = DataSource.getDataSource(context.resources) - ) as T +class TopicsViewModelFactory() : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class) = + with(modelClass){ + when { + isAssignableFrom(TopicsViewModel::class.java) -> TopicsViewModel(TopicsRepository.getInstance()) as T + else -> throw IllegalArgumentException("Unknown viewModel class $modelClass") + } } - throw IllegalArgumentException("Unknown ViewModel class") - } } diff --git a/app/src/main/java/io/heckel/ntfy/data/DataSource.kt b/app/src/main/java/io/heckel/ntfy/data/DataSource.kt deleted file mode 100644 index 20d2a60..0000000 --- a/app/src/main/java/io/heckel/ntfy/data/DataSource.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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.data - -import android.content.res.Resources -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData - -/* Handles operations on topicsLiveData and holds details about it. */ -class DataSource(resources: Resources) { - private val topicsLiveData: MutableLiveData> = MutableLiveData(mutableListOf()) - - /* Adds topic to liveData and posts value. */ - fun add(topic: Topic) { - val currentList = topicsLiveData.value - if (currentList == null) { - topicsLiveData.postValue(listOf(topic)) - } else { - val updatedList = currentList.toMutableList() - updatedList.add(0, topic) - topicsLiveData.postValue(updatedList) - } - } - - /* Removes topic from liveData and posts value. */ - fun remove(topic: Topic) { - val currentList = topicsLiveData.value - if (currentList != null) { - val updatedList = currentList.toMutableList() - updatedList.remove(topic) - topicsLiveData.postValue(updatedList) - } - } - - /* Returns topic given an ID. */ - fun get(id: Long): Topic? { - topicsLiveData.value?.let { topics -> - return topics.firstOrNull{ it.id == id} - } - return null - } - - fun list(): LiveData> { - return topicsLiveData - } - - companion object { - private var instance: DataSource? = null - - fun getDataSource(resources: Resources): DataSource { - return synchronized(DataSource::class) { - val newInstance = instance ?: DataSource(resources) - instance = newInstance - newInstance - } - } - } -} diff --git a/app/src/main/java/io/heckel/ntfy/data/TopicsRepository.kt b/app/src/main/java/io/heckel/ntfy/data/TopicsRepository.kt new file mode 100644 index 0000000..6b08989 --- /dev/null +++ b/app/src/main/java/io/heckel/ntfy/data/TopicsRepository.kt @@ -0,0 +1,109 @@ +package io.heckel.ntfy.data + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.google.gson.GsonBuilder +import com.google.gson.JsonObject +import com.google.gson.JsonSyntaxException +import io.heckel.ntfy.Notification +import io.heckel.ntfy.NotificationListener +import kotlinx.coroutines.* +import java.io.IOException +import java.net.HttpURLConnection +import java.net.URL + +/* Handles operations on topicsLiveData and holds details about it. */ +class TopicsRepository { + private val topics: MutableLiveData> = MutableLiveData(mutableListOf()) + private val jobs = mutableMapOf() + private val gson = GsonBuilder().create() + private var notificationListener: NotificationListener? = null; + + /* Adds topic to liveData and posts value. */ + fun add(topic: Topic, scope: CoroutineScope) { + val currentList = topics.value + if (currentList == null) { + topics.postValue(listOf(topic)) + } else { + val updatedList = currentList.toMutableList() + updatedList.add(0, topic) + topics.postValue(updatedList) + } + jobs[topic.id] = subscribeTopic(topic, scope) + } + + /* Removes topic from liveData and posts value. */ + fun remove(topic: Topic) { + val currentList = topics.value + if (currentList != null) { + val updatedList = currentList.toMutableList() + updatedList.remove(topic) + topics.postValue(updatedList) + } + jobs.remove(topic.id)?.cancel() // Cancel and remove + } + + /* Returns topic given an ID. */ + fun get(id: Long): Topic? { + topics.value?.let { topics -> + return topics.firstOrNull{ it.id == id} + } + return null + } + + fun list(): LiveData> { + return topics + } + + fun setNotificationListener(listener: NotificationListener) { + notificationListener = listener + } + + private fun subscribeTopic(topic: Topic, scope: CoroutineScope): Job { + return scope.launch(Dispatchers.IO) { + while (isActive) { + openURL(this, topic.url, topic.url) // TODO + delay(5000) // TODO exponential back-off + } + } + } + + private fun openURL(scope: CoroutineScope, topic: String, 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 // Exit if null + try { + val json = gson.fromJson(line, JsonObject::class.java) ?: break // Exit if null + if (!json.isJsonNull && json.has("message")) { + val message = json.get("message").asString + notificationListener?.let { it(Notification(url, message)) } + } + } catch (e: JsonSyntaxException) { + // Ignore invalid JSON + } + } + } catch (e: IOException) { + println("PHIL: " + e.message) + } finally { + conn.disconnect() + } + println("Connection terminated: $url") + } + + companion object { + private var instance: TopicsRepository? = null + + fun getInstance(): TopicsRepository { + return synchronized(TopicsRepository::class) { + val newInstance = instance ?: TopicsRepository() + instance = newInstance + newInstance + } + } + } +} diff --git a/app/src/main/java/io/heckel/ntfy/detail/DetailActivity.kt b/app/src/main/java/io/heckel/ntfy/detail/DetailActivity.kt index 4127936..441160e 100644 --- a/app/src/main/java/io/heckel/ntfy/detail/DetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/detail/DetailActivity.kt @@ -23,12 +23,12 @@ import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import io.heckel.ntfy.R import io.heckel.ntfy.TOPIC_ID -import io.heckel.ntfy.list.TopicsViewModel -import io.heckel.ntfy.list.TopicsViewModelFactory +import io.heckel.ntfy.TopicsViewModel +import io.heckel.ntfy.TopicsViewModelFactory class DetailActivity : AppCompatActivity() { private val topicsViewModel by viewModels { - TopicsViewModelFactory(this) + TopicsViewModelFactory() } override fun onCreate(savedInstanceState: Bundle?) {