Notification action when clicked, add more validation on add topic
This commit is contained in:
parent
2a64f44916
commit
7d561a5068
9 changed files with 96 additions and 42 deletions
|
@ -25,7 +25,9 @@
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- Detail activity -->
|
<!-- Detail activity -->
|
||||||
<activity android:name=".ui.DetailActivity">
|
<activity
|
||||||
|
android:name=".ui.DetailActivity"
|
||||||
|
android:parentActivityName=".ui.MainActivity">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value=".ui.MainActivity" />
|
android:value=".ui.MainActivity" />
|
||||||
|
|
|
@ -2,7 +2,10 @@ package io.heckel.ntfy.msg
|
||||||
|
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.TaskStackBuilder
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.media.RingtoneManager
|
import android.media.RingtoneManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
@ -10,10 +13,9 @@ import androidx.core.app.NotificationCompat
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
import com.google.firebase.messaging.RemoteMessage
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.data.Database
|
import io.heckel.ntfy.data.*
|
||||||
import io.heckel.ntfy.data.Notification
|
import io.heckel.ntfy.ui.DetailActivity
|
||||||
import io.heckel.ntfy.data.Repository
|
import io.heckel.ntfy.ui.MainActivity
|
||||||
import io.heckel.ntfy.data.topicShortUrl
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -56,8 +58,7 @@ class MessagingService : FirebaseMessagingService() {
|
||||||
|
|
||||||
// Send notification
|
// Send notification
|
||||||
Log.d(TAG, "Sending notification for message: from=${remoteMessage.from}, data=${data}")
|
Log.d(TAG, "Sending notification for message: from=${remoteMessage.from}, data=${data}")
|
||||||
val title = topicShortUrl(baseUrl, topic)
|
sendNotification(subscription, message)
|
||||||
sendNotification(title, message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +72,19 @@ class MessagingService : FirebaseMessagingService() {
|
||||||
job.cancel()
|
job.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendNotification(title: String, message: String) {
|
private fun sendNotification(subscription: Subscription, message: String) {
|
||||||
|
val title = topicShortUrl(subscription.baseUrl, subscription.topic)
|
||||||
|
|
||||||
|
// Create an Intent for the activity you want to start
|
||||||
|
val intent = Intent(this, DetailActivity::class.java)
|
||||||
|
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id)
|
||||||
|
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl)
|
||||||
|
intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC, subscription.topic)
|
||||||
|
val pendingIntent: PendingIntent? = TaskStackBuilder.create(this).run {
|
||||||
|
addNextIntentWithParentStack(intent) // Add the intent, which inflates the back stack
|
||||||
|
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) // Get the PendingIntent containing the entire back stack
|
||||||
|
}
|
||||||
|
|
||||||
val channelId = getString(R.string.notification_channel_id)
|
val channelId = getString(R.string.notification_channel_id)
|
||||||
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
|
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
|
||||||
val notificationBuilder = NotificationCompat.Builder(this, channelId)
|
val notificationBuilder = NotificationCompat.Builder(this, channelId)
|
||||||
|
@ -79,6 +92,9 @@ class MessagingService : FirebaseMessagingService() {
|
||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
.setContentText(message)
|
.setContentText(message)
|
||||||
.setSound(defaultSoundUri)
|
.setSound(defaultSoundUri)
|
||||||
|
.setContentIntent(pendingIntent) // Click target for notification
|
||||||
|
.setAutoCancel(true) // Cancel when notification is clicked
|
||||||
|
|
||||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val channelName = getString(R.string.notification_channel_name)
|
val channelName = getString(R.string.notification_channel_name)
|
||||||
|
|
|
@ -1,28 +1,38 @@
|
||||||
package io.heckel.ntfy.ui
|
package io.heckel.ntfy.ui
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.Button
|
||||||
import android.widget.CheckBox
|
import android.widget.CheckBox
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
|
import io.heckel.ntfy.app.Application
|
||||||
|
import io.heckel.ntfy.data.Repository
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class AddFragment(private val listener: AddSubscriptionListener) : DialogFragment() {
|
class AddFragment(private val viewModel: SubscriptionsViewModel, private val onSubscribe: (topic: String, baseUrl: String) -> Unit) : DialogFragment() {
|
||||||
interface AddSubscriptionListener {
|
private lateinit var topicNameText: TextInputEditText
|
||||||
fun onSubscribe(topic: String, baseUrl: String)
|
private lateinit var baseUrlText: TextInputEditText
|
||||||
}
|
private lateinit var useAnotherServerCheckbox: CheckBox
|
||||||
|
private lateinit var subscribeButton: Button
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
return activity?.let {
|
return activity?.let {
|
||||||
// Build root view
|
// Build root view
|
||||||
val view = requireActivity().layoutInflater.inflate(R.layout.add_dialog_fragment, null)
|
val view = requireActivity().layoutInflater.inflate(R.layout.add_dialog_fragment, null)
|
||||||
val topicNameText = view.findViewById(R.id.add_dialog_topic_text) as TextInputEditText
|
topicNameText = view.findViewById(R.id.add_dialog_topic_text) as TextInputEditText
|
||||||
val baseUrlText = view.findViewById(R.id.add_dialog_base_url_text) as TextInputEditText
|
baseUrlText = view.findViewById(R.id.add_dialog_base_url_text) as TextInputEditText
|
||||||
val useAnotherServerCheckbox = view.findViewById(R.id.add_dialog_use_another_server_checkbox) as CheckBox
|
useAnotherServerCheckbox = view.findViewById(R.id.add_dialog_use_another_server_checkbox) as CheckBox
|
||||||
|
|
||||||
// FIXME For now, other servers are disabled
|
// FIXME For now, other servers are disabled
|
||||||
useAnotherServerCheckbox.visibility = View.GONE
|
useAnotherServerCheckbox.visibility = View.GONE
|
||||||
|
@ -32,12 +42,8 @@ class AddFragment(private val listener: AddSubscriptionListener) : DialogFragmen
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setPositiveButton(R.string.add_dialog_button_subscribe) { _, _ ->
|
.setPositiveButton(R.string.add_dialog_button_subscribe) { _, _ ->
|
||||||
val topic = topicNameText.text.toString()
|
val topic = topicNameText.text.toString()
|
||||||
val baseUrl = if (useAnotherServerCheckbox.isChecked) {
|
val baseUrl = getBaseUrl()
|
||||||
baseUrlText.text.toString()
|
onSubscribe(topic, baseUrl)
|
||||||
} else {
|
|
||||||
getString(R.string.app_base_url)
|
|
||||||
}
|
|
||||||
listener.onSubscribe(topic, baseUrl)
|
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.add_dialog_button_cancel) { _, _ ->
|
.setNegativeButton(R.string.add_dialog_button_cancel) { _, _ ->
|
||||||
dialog?.cancel()
|
dialog?.cancel()
|
||||||
|
@ -48,20 +54,9 @@ class AddFragment(private val listener: AddSubscriptionListener) : DialogFragmen
|
||||||
alert.setOnShowListener {
|
alert.setOnShowListener {
|
||||||
val dialog = it as AlertDialog
|
val dialog = it as AlertDialog
|
||||||
|
|
||||||
val subscribeButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
subscribeButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||||
subscribeButton.isEnabled = false
|
subscribeButton.isEnabled = false
|
||||||
|
|
||||||
val validateInput: () -> Unit = {
|
|
||||||
if (useAnotherServerCheckbox.isChecked) {
|
|
||||||
subscribeButton.isEnabled = topicNameText.text.toString().isNotBlank()
|
|
||||||
&& "[-_A-Za-z0-9]+".toRegex().matches(topicNameText.text.toString())
|
|
||||||
&& baseUrlText.text.toString().isNotBlank()
|
|
||||||
&& "^https?://.+".toRegex().matches(baseUrlText.text.toString())
|
|
||||||
} else {
|
|
||||||
subscribeButton.isEnabled = topicNameText.text.toString().isNotBlank()
|
|
||||||
&& "[-_A-Za-z0-9]+".toRegex().matches(topicNameText.text.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val textWatcher = object : TextWatcher {
|
val textWatcher = object : TextWatcher {
|
||||||
override fun afterTextChanged(s: Editable?) {
|
override fun afterTextChanged(s: Editable?) {
|
||||||
validateInput()
|
validateInput()
|
||||||
|
@ -85,4 +80,35 @@ class AddFragment(private val listener: AddSubscriptionListener) : DialogFragmen
|
||||||
alert
|
alert
|
||||||
} ?: throw IllegalStateException("Activity cannot be null")
|
} ?: throw IllegalStateException("Activity cannot be null")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun validateInput() = lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val baseUrl = getBaseUrl()
|
||||||
|
val topic = topicNameText.text.toString()
|
||||||
|
val subscription = viewModel.get(baseUrl, topic)
|
||||||
|
|
||||||
|
println("sub $subscription")
|
||||||
|
activity?.let {
|
||||||
|
it.runOnUiThread {
|
||||||
|
if (subscription != null) {
|
||||||
|
subscribeButton.isEnabled = false
|
||||||
|
} else if (useAnotherServerCheckbox.isChecked) {
|
||||||
|
subscribeButton.isEnabled = topic.isNotBlank()
|
||||||
|
&& "[-_A-Za-z0-9]+".toRegex().matches(topic)
|
||||||
|
&& baseUrl.isNotBlank()
|
||||||
|
&& "^https?://.+".toRegex().matches(baseUrl)
|
||||||
|
} else {
|
||||||
|
subscribeButton.isEnabled = topic.isNotBlank()
|
||||||
|
&& "[-_A-Za-z0-9]+".toRegex().matches(topic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBaseUrl(): String {
|
||||||
|
return if (useAnotherServerCheckbox.isChecked) {
|
||||||
|
baseUrlText.text.toString()
|
||||||
|
} else {
|
||||||
|
getString(R.string.app_base_url)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,16 +8,19 @@ import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.firebase.messaging.FirebaseMessaging
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.app.Application
|
import io.heckel.ntfy.app.Application
|
||||||
import io.heckel.ntfy.data.Subscription
|
import io.heckel.ntfy.data.Subscription
|
||||||
import io.heckel.ntfy.data.topicShortUrl
|
import io.heckel.ntfy.data.topicShortUrl
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener {
|
class MainActivity : AppCompatActivity() {
|
||||||
private val viewModel by viewModels<SubscriptionsViewModel> {
|
private val viewModel by viewModels<SubscriptionsViewModel> {
|
||||||
SubscriptionsViewModelFactory((application as Application).repository)
|
SubscriptionsViewModelFactory((application as Application).repository)
|
||||||
}
|
}
|
||||||
|
@ -75,11 +78,11 @@ class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSubscribeButtonClick() {
|
private fun onSubscribeButtonClick() {
|
||||||
val newFragment = AddFragment(this)
|
val newFragment = AddFragment(viewModel) { topic, baseUrl -> onSubscribe(topic, baseUrl) }
|
||||||
newFragment.show(supportFragmentManager, "AddFragment")
|
newFragment.show(supportFragmentManager, "AddFragment")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSubscribe(topic: String, baseUrl: String) {
|
private fun onSubscribe(topic: String, baseUrl: String) {
|
||||||
val subscription = Subscription(id = Random.nextLong(), baseUrl = baseUrl, topic = topic, notifications = 0, lastActive = Date().time/1000)
|
val subscription = Subscription(id = Random.nextLong(), baseUrl = baseUrl, topic = topic, notifications = 0, lastActive = Date().time/1000)
|
||||||
viewModel.add(subscription)
|
viewModel.add(subscription)
|
||||||
FirebaseMessaging.getInstance().subscribeToTopic(topic) // FIXME ignores baseUrl
|
FirebaseMessaging.getInstance().subscribeToTopic(topic) // FIXME ignores baseUrl
|
||||||
|
|
|
@ -21,6 +21,10 @@ class SubscriptionsViewModel(private val repository: Repository) : ViewModel() {
|
||||||
fun remove(subscriptionId: Long) = viewModelScope.launch(Dispatchers.IO) {
|
fun remove(subscriptionId: Long) = viewModelScope.launch(Dispatchers.IO) {
|
||||||
repository.removeSubscription(subscriptionId)
|
repository.removeSubscription(subscriptionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun get(baseUrl: String, topic: String): Subscription? {
|
||||||
|
return repository.getSubscription(baseUrl, topic)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubscriptionsViewModelFactory(private val repository: Repository) : ViewModelProvider.Factory {
|
class SubscriptionsViewModelFactory(private val repository: Repository) : ViewModelProvider.Factory {
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
app:layoutManager="LinearLayoutManager" android:visibility="gone"/>
|
app:layoutManager="LinearLayoutManager" android:visibility="gone"/>
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,8 @@
|
||||||
android:id="@+id/detail_item_date_text"
|
android:id="@+id/detail_item_date_text"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="10dp"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"/>
|
||||||
<TextView
|
<TextView
|
||||||
android:text="This is a very very very long message. It could be as long as 1024 charaters, which is a lot more than you'd think. No, really so far this message is barely 180 characters long. I can't believe how long 1024 bytes are. This is outrageous. Oh you know what, I think I won't type the whole thing. This seems a little too long for a sample text. Well, anyway, it was nice chatting. So far this message is about 400 bytes long. So maybe just double what you see and that's that."
|
android:text="This is a very very very long message. It could be as long as 1024 charaters, which is a lot more than you'd think. No, really so far this message is barely 180 characters long. I can't believe how long 1024 bytes are. This is outrageous. Oh you know what, I think I won't type the whole thing. This seems a little too long for a sample text. Well, anyway, it was nice chatting. So far this message is about 400 bytes long. So maybe just double what you see and that's that."
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
android:id="@+id/detail_item_message_text"
|
android:id="@+id/detail_item_message_text"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="10dp"
|
||||||
android:textColor="@color/primaryTextColor"
|
android:textColor="@color/primaryTextColor"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>
|
android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
app:layoutManager="LinearLayoutManager" android:visibility="gone"/>
|
app:layoutManager="LinearLayoutManager" android:visibility="gone"/>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<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"
|
xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent"
|
||||||
android:layout_height="80dp"
|
android:layout_height="wrap_content"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:orientation="horizontal" android:clickable="true" android:focusable="true">
|
android:orientation="horizontal" android:clickable="true" android:focusable="true">
|
||||||
<ImageView
|
<ImageView
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
android:text="ntfy.sh/example"
|
android:text="ntfy.sh/example"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" android:id="@+id/main_item_text"
|
android:layout_height="wrap_content" android:id="@+id/main_item_text"
|
||||||
android:layout_marginTop="16dp" android:layout_marginStart="12dp"
|
android:layout_marginTop="10dp" android:layout_marginStart="12dp"
|
||||||
android:textColor="@color/primaryTextColor"
|
android:textColor="@color/primaryTextColor"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>
|
android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>
|
||||||
|
|
||||||
|
@ -24,7 +24,8 @@
|
||||||
android:text="Subscribed, 0 notifications"
|
android:text="Subscribed, 0 notifications"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" android:id="@+id/main_item_status"
|
android:layout_height="wrap_content" android:id="@+id/main_item_status"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" android:layout_marginStart="12dp"/>
|
android:textAppearance="@style/TextAppearance.AppCompat.Small" android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginBottom="10dp"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
Loading…
Reference in a new issue