Notification action when clicked, add more validation on add topic

This commit is contained in:
Philipp Heckel 2021-11-01 08:58:12 -04:00
parent 2a64f44916
commit 7d561a5068
9 changed files with 96 additions and 42 deletions

View file

@ -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" />

View file

@ -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)

View file

@ -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)
}
}
} }

View file

@ -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

View file

@ -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 {

View file

@ -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"/>

View file

@ -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"/>

View file

@ -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"/>

View file

@ -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>