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>
<!-- Detail activity -->
<activity android:name=".ui.DetailActivity">
<activity
android:name=".ui.DetailActivity"
android:parentActivityName=".ui.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.MainActivity" />

View file

@ -2,7 +2,10 @@ package io.heckel.ntfy.msg
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.TaskStackBuilder
import android.content.Context
import android.content.Intent
import android.media.RingtoneManager
import android.os.Build
import android.util.Log
@ -10,10 +13,9 @@ import androidx.core.app.NotificationCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import io.heckel.ntfy.R
import io.heckel.ntfy.data.Database
import io.heckel.ntfy.data.Notification
import io.heckel.ntfy.data.Repository
import io.heckel.ntfy.data.topicShortUrl
import io.heckel.ntfy.data.*
import io.heckel.ntfy.ui.DetailActivity
import io.heckel.ntfy.ui.MainActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
@ -56,8 +58,7 @@ class MessagingService : FirebaseMessagingService() {
// Send notification
Log.d(TAG, "Sending notification for message: from=${remoteMessage.from}, data=${data}")
val title = topicShortUrl(baseUrl, topic)
sendNotification(title, message)
sendNotification(subscription, message)
}
}
@ -71,7 +72,19 @@ class MessagingService : FirebaseMessagingService() {
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 defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val notificationBuilder = NotificationCompat.Builder(this, channelId)
@ -79,6 +92,9 @@ class MessagingService : FirebaseMessagingService() {
.setContentTitle(title)
.setContentText(message)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent) // Click target for notification
.setAutoCancel(true) // Cancel when notification is clicked
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelName = getString(R.string.notification_channel_name)

View file

@ -1,28 +1,38 @@
package io.heckel.ntfy.ui
import android.app.Activity
import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.widget.Button
import android.widget.CheckBox
import androidx.activity.viewModels
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import com.google.android.material.textfield.TextInputEditText
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() {
interface AddSubscriptionListener {
fun onSubscribe(topic: String, baseUrl: String)
}
class AddFragment(private val viewModel: SubscriptionsViewModel, private val onSubscribe: (topic: String, baseUrl: String) -> Unit) : DialogFragment() {
private lateinit var topicNameText: TextInputEditText
private lateinit var baseUrlText: TextInputEditText
private lateinit var useAnotherServerCheckbox: CheckBox
private lateinit var subscribeButton: Button
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
// Build root view
val view = requireActivity().layoutInflater.inflate(R.layout.add_dialog_fragment, null)
val topicNameText = view.findViewById(R.id.add_dialog_topic_text) as TextInputEditText
val 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
topicNameText = view.findViewById(R.id.add_dialog_topic_text) as TextInputEditText
baseUrlText = view.findViewById(R.id.add_dialog_base_url_text) as TextInputEditText
useAnotherServerCheckbox = view.findViewById(R.id.add_dialog_use_another_server_checkbox) as CheckBox
// FIXME For now, other servers are disabled
useAnotherServerCheckbox.visibility = View.GONE
@ -32,12 +42,8 @@ class AddFragment(private val listener: AddSubscriptionListener) : DialogFragmen
.setView(view)
.setPositiveButton(R.string.add_dialog_button_subscribe) { _, _ ->
val topic = topicNameText.text.toString()
val baseUrl = if (useAnotherServerCheckbox.isChecked) {
baseUrlText.text.toString()
} else {
getString(R.string.app_base_url)
}
listener.onSubscribe(topic, baseUrl)
val baseUrl = getBaseUrl()
onSubscribe(topic, baseUrl)
}
.setNegativeButton(R.string.add_dialog_button_cancel) { _, _ ->
dialog?.cancel()
@ -48,20 +54,9 @@ class AddFragment(private val listener: AddSubscriptionListener) : DialogFragmen
alert.setOnShowListener {
val dialog = it as AlertDialog
val subscribeButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
subscribeButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
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 {
override fun afterTextChanged(s: Editable?) {
validateInput()
@ -85,4 +80,35 @@ class AddFragment(private val listener: AddSubscriptionListener) : DialogFragmen
alert
} ?: 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 androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.google.firebase.messaging.FirebaseMessaging
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.data.topicShortUrl
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
import kotlin.random.Random
class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener {
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<SubscriptionsViewModel> {
SubscriptionsViewModelFactory((application as Application).repository)
}
@ -75,11 +78,11 @@ class MainActivity : AppCompatActivity(), AddFragment.AddSubscriptionListener {
}
private fun onSubscribeButtonClick() {
val newFragment = AddFragment(this)
val newFragment = AddFragment(viewModel) { topic, baseUrl -> onSubscribe(topic, baseUrl) }
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)
viewModel.add(subscription)
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) {
repository.removeSubscription(subscriptionId)
}
suspend fun get(baseUrl: String, topic: String): Subscription? {
return repository.getSubscription(baseUrl, topic)
}
}
class SubscriptionsViewModelFactory(private val repository: Repository) : ViewModelProvider.Factory {

View file

@ -13,6 +13,7 @@
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true"
android:layout_marginTop="10dp"
android:background="?android:attr/selectableItemBackground"
app:layoutManager="LinearLayoutManager" android:visibility="gone"/>

View file

@ -11,8 +11,8 @@
android:id="@+id/detail_item_date_text"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
android:layout_marginTop="10dp"
android:textAppearance="@style/TextAppearance.AppCompat.Small"/>
<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:layout_width="match_parent"
@ -20,7 +20,7 @@
android:id="@+id/detail_item_message_text"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:layout_marginBottom="10dp"
android:textColor="@color/primaryTextColor"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>

View file

@ -10,6 +10,7 @@
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true"
android:layout_marginTop="10dp"
android:background="?android:attr/selectableItemBackground"
app:layoutManager="LinearLayoutManager" android:visibility="gone"/>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
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:orientation="horizontal" android:clickable="true" android:focusable="true">
<ImageView
@ -16,7 +16,7 @@
android:text="ntfy.sh/example"
android:layout_width="match_parent"
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:textAppearance="@style/TextAppearance.AppCompat.Medium"/>
@ -24,7 +24,8 @@
android:text="Subscribed, 0 notifications"
android:layout_width="match_parent"
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>