Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 49b38989 authored by Philipp Heckel's avatar Philipp Heckel
Browse files

We're getting there

parent 638c8f09
Loading
Loading
Loading
Loading
+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
@@ -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)
            }
+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
@@ -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
    }
}
+3 −4
Original line number Diff line number Diff line
@@ -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")
            }
        }
+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
@@ -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()
+18 −13
Original line number Diff line number Diff line
@@ -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
            }
Loading