We're getting there
This commit is contained in:
parent
638c8f093a
commit
49b3898977
10 changed files with 91 additions and 144 deletions
|
@ -1,19 +1,3 @@
|
|||
/*
|
||||
* 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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -1,20 +1,4 @@
|
|||
/*
|
||||
* 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
|
||||
|
@ -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) :
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,3 @@
|
|||
/*
|
||||
* 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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
|
@ -1,22 +1,7 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
data class Topic(
|
||||
val id: Long,
|
||||
val url: String,
|
||||
val id: Long, // Internal to Repository only
|
||||
val name: String,
|
||||
val baseUrl: String,
|
||||
)
|
||||
|
|
|
@ -35,30 +35,31 @@ class DetailActivity : AppCompatActivity() {
|
|||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.topic_detail_activity)
|
||||
|
||||
var currentTopicId: Long? = null
|
||||
var topicId: Long? = null
|
||||
|
||||
/* Connect variables to UI elements. */
|
||||
val topicUrl: TextView = findViewById(R.id.topic_detail_url)
|
||||
val removeTopicButton: Button = findViewById(R.id.remove_button)
|
||||
val topicText: TextView = findViewById(R.id.topic_detail_url)
|
||||
val removeButton: Button = findViewById(R.id.remove_button)
|
||||
|
||||
val bundle: Bundle? = intent.extras
|
||||
if (bundle != null) {
|
||||
currentTopicId = bundle.getLong(TOPIC_ID)
|
||||
topicId = bundle.getLong(TOPIC_ID)
|
||||
}
|
||||
|
||||
// TODO This should probably fail hard if topicId is null
|
||||
|
||||
/* If currentTopicId is not null, get corresponding topic and set name, image and
|
||||
description */
|
||||
currentTopicId?.let {
|
||||
val currentTopic = topicsViewModel.get(it)
|
||||
topicUrl.text = currentTopic?.url
|
||||
topicId?.let {
|
||||
val topic = topicsViewModel.get(it)
|
||||
topicText.text = "${topic?.baseUrl}/${topic?.name}"
|
||||
|
||||
removeTopicButton.setOnClickListener {
|
||||
if (currentTopic != null) {
|
||||
topicsViewModel.remove(currentTopic)
|
||||
removeButton.setOnClickListener {
|
||||
if (topic != null) {
|
||||
topicsViewModel.remove(topic)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,27 +20,40 @@
|
|||
android:paddingRight="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:text="@string/add_topic"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="?attr/textAppearanceHeadline4" />
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:text="@string/add_topic"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="?attr/textAppearanceHeadline4"/>
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:hint="@string/topic_name_edit_text"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/add_topic_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:hint="@string/topic_name_edit_text"
|
||||
android:hint="@string/topic_base_url_edit_text"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/add_topic_url"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
android:id="@+id/add_topic_base_url"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
|
|
@ -16,13 +16,13 @@
|
|||
android:layout_height="80dp"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:text="https://ntfy.sh/example"
|
||||
android:text="ntfy.sh/example"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/topic_text"
|
||||
android:layout_marginTop="16dp" android:layout_marginLeft="16dp"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>
|
||||
<TextView
|
||||
android:text="Subscribed, 0 messages"
|
||||
android:text="Subscribed, 0 notifications"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/topic_status"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" android:layout_marginLeft="16dp"/>
|
||||
|
|
|
@ -1,25 +1,12 @@
|
|||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="app_name">Ntfy</string>
|
||||
<string name="add_topic">Add Topic</string>
|
||||
|
||||
<string name="topic_string">Topics</string>
|
||||
<string name="topic_name_edit_text">Topic URL</string>
|
||||
<string name="topic_name_edit_text">Topic Name</string>
|
||||
<string name="topic_base_url_edit_text">Service URL</string>
|
||||
<string name="topic_base_url_default_value">https://ntfy.sh</string>
|
||||
<string name="topic_short_name_format">%1$s/%2$s</string>
|
||||
<string name="subscribe_button_text">Subscribe</string>
|
||||
<string name="fab_content_description">fab</string>
|
||||
<string name="remove_topic">Unsubscribe</string>
|
||||
|
|
Loading…
Reference in a new issue