Compare commits
25 commits
Author | SHA1 | Date | |
---|---|---|---|
|
2318e28f11 | ||
|
bb6adde03a | ||
|
6f9d4bff5a | ||
|
bcec43769b | ||
|
2def9da084 | ||
|
aea2d6b289 | ||
|
0571096d44 | ||
|
a9fa4fa0bf | ||
|
3e28ee2722 | ||
|
a6cd89247c | ||
|
125fe48297 | ||
|
2cf668e685 | ||
|
a44f572fb4 | ||
|
409086e32a | ||
|
a434a1d722 | ||
|
37be787eb8 | ||
|
5f780c27fb | ||
|
67a80d738c | ||
|
484c66b0b5 | ||
|
e0e2edf8a0 | ||
|
8af874a499 | ||
|
182819670b | ||
|
085f0d51e8 | ||
|
5b39062c4a | ||
|
3486333c2a |
|
@ -41,14 +41,7 @@ android {
|
||||||
|
|
||||||
flavorDimensions "store"
|
flavorDimensions "store"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
play {
|
|
||||||
buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'true'
|
|
||||||
buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'true'
|
|
||||||
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'false'
|
|
||||||
}
|
|
||||||
fdroid {
|
fdroid {
|
||||||
buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'false'
|
|
||||||
buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'false'
|
|
||||||
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'true'
|
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'true'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,9 +103,6 @@ dependencies {
|
||||||
// OkHttp (HTTP library)
|
// OkHttp (HTTP library)
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
||||||
|
|
||||||
// Firebase, sigh ... (only Google Play)
|
|
||||||
playImplementation 'com.google.firebase:firebase-messaging:23.1.2'
|
|
||||||
|
|
||||||
// RecyclerView
|
// RecyclerView
|
||||||
implementation "androidx.recyclerview:recyclerview:1.3.0"
|
implementation "androidx.recyclerview:recyclerview:1.3.0"
|
||||||
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
package io.heckel.ntfy.firebase
|
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
class FirebaseMessenger {
|
|
||||||
fun subscribe(topic: String) {
|
|
||||||
// Dummy to keep F-Droid flavor happy
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unsubscribe(topic: String) {
|
|
||||||
// Dummy to keep F-Droid flavor happy
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
package io.heckel.ntfy.firebase
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.IBinder
|
|
||||||
|
|
||||||
// Dummy to keep F-Droid flavor happy
|
|
||||||
class FirebaseService : Service() {
|
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -23,9 +23,9 @@
|
||||||
<application
|
<application
|
||||||
android:name=".app.Application"
|
android:name=".app.Application"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_murena"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher"
|
android:roundIcon="@mipmap/ic_murena_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
@ -132,6 +132,7 @@
|
||||||
<action android:name="org.unifiedpush.android.distributor.REGISTER"/>
|
<action android:name="org.unifiedpush.android.distributor.REGISTER"/>
|
||||||
<action android:name="org.unifiedpush.android.distributor.UNREGISTER"/>
|
<action android:name="org.unifiedpush.android.distributor.UNREGISTER"/>
|
||||||
<action android:name="org.unifiedpush.android.distributor.feature.BYTES_MESSAGE"/>
|
<action android:name="org.unifiedpush.android.distributor.feature.BYTES_MESSAGE"/>
|
||||||
|
<action android:name="org.unifiedpush.android.distributor.feature.MURENA"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
|
BIN
app/src/main/ic_murena-playstore.png
Normal file
After Width: | Height: | Size: 104 KiB |
|
@ -8,7 +8,6 @@ import com.google.gson.stream.JsonReader
|
||||||
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.db.Repository
|
import io.heckel.ntfy.db.Repository
|
||||||
import io.heckel.ntfy.firebase.FirebaseMessenger
|
|
||||||
import io.heckel.ntfy.msg.NotificationService
|
import io.heckel.ntfy.msg.NotificationService
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
import io.heckel.ntfy.util.topicUrl
|
import io.heckel.ntfy.util.topicUrl
|
||||||
|
@ -18,7 +17,6 @@ class Backuper(val context: Context) {
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
private val resolver = context.applicationContext.contentResolver
|
private val resolver = context.applicationContext.contentResolver
|
||||||
private val repository = (context.applicationContext as Application).repository
|
private val repository = (context.applicationContext as Application).repository
|
||||||
private val messenger = FirebaseMessenger()
|
|
||||||
private val notifier = NotificationService(context)
|
private val notifier = NotificationService(context)
|
||||||
|
|
||||||
suspend fun backup(uri: Uri, withSettings: Boolean = true, withSubscriptions: Boolean = true, withUsers: Boolean = true) {
|
suspend fun backup(uri: Uri, withSettings: Boolean = true, withSubscriptions: Boolean = true, withUsers: Boolean = true) {
|
||||||
|
@ -114,11 +112,6 @@ class Backuper(val context: Context) {
|
||||||
)
|
)
|
||||||
repository.addSubscription(subscription)
|
repository.addSubscription(subscription)
|
||||||
|
|
||||||
// Subscribe to Firebase topics
|
|
||||||
if (s.baseUrl == appBaseUrl) {
|
|
||||||
messenger.subscribe(s.topic)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create dedicated channels
|
// Create dedicated channels
|
||||||
if (s.dedicatedChannels) {
|
if (s.dedicatedChannels) {
|
||||||
notifier.createSubscriptionNotificationChannels(subscription)
|
notifier.createSubscriptionNotificationChannels(subscription)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import io.heckel.ntfy.db.Subscription
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
import io.heckel.ntfy.up.Distributor
|
import io.heckel.ntfy.up.Distributor
|
||||||
import io.heckel.ntfy.util.decodeBytesMessage
|
import io.heckel.ntfy.util.decodeBytesMessage
|
||||||
|
import io.heckel.ntfy.util.decodeBytesTitle
|
||||||
import io.heckel.ntfy.util.safeLet
|
import io.heckel.ntfy.util.safeLet
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,7 +40,12 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
|
||||||
}
|
}
|
||||||
if (distribute) {
|
if (distribute) {
|
||||||
safeLet(subscription.upAppId, subscription.upConnectorToken) { appId, connectorToken ->
|
safeLet(subscription.upAppId, subscription.upConnectorToken) { appId, connectorToken ->
|
||||||
distributor.sendMessage(appId, connectorToken, decodeBytesMessage(notification))
|
distributor.sendMessage(
|
||||||
|
appId,
|
||||||
|
connectorToken,
|
||||||
|
decodeBytesMessage(notification),
|
||||||
|
decodeBytesTitle(notification),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (downloadAttachment && downloadIcon) {
|
if (downloadAttachment && downloadIcon) {
|
||||||
|
|
|
@ -90,11 +90,7 @@ class SubscriberService : Service() {
|
||||||
Log.d(TAG, "Subscriber service has been created")
|
Log.d(TAG, "Subscriber service has been created")
|
||||||
|
|
||||||
val title = getString(R.string.channel_subscriber_notification_title)
|
val title = getString(R.string.channel_subscriber_notification_title)
|
||||||
val text = if (BuildConfig.FIREBASE_AVAILABLE) {
|
val text = getString(R.string.channel_subscriber_notification_noinstant_text)
|
||||||
getString(R.string.channel_subscriber_notification_instant_text)
|
|
||||||
} else {
|
|
||||||
getString(R.string.channel_subscriber_notification_noinstant_text)
|
|
||||||
}
|
|
||||||
notificationManager = createNotificationChannel()
|
notificationManager = createNotificationChannel()
|
||||||
serviceNotification = createNotification(title, text)
|
serviceNotification = createNotification(title, text)
|
||||||
|
|
||||||
|
@ -221,18 +217,7 @@ class SubscriberService : Service() {
|
||||||
// Update foreground service notification popup
|
// Update foreground service notification popup
|
||||||
if (connections.size > 0) {
|
if (connections.size > 0) {
|
||||||
val title = getString(R.string.channel_subscriber_notification_title)
|
val title = getString(R.string.channel_subscriber_notification_title)
|
||||||
val text = if (BuildConfig.FIREBASE_AVAILABLE) {
|
val text = when (instantSubscriptions.size) {
|
||||||
when (instantSubscriptions.size) {
|
|
||||||
1 -> getString(R.string.channel_subscriber_notification_instant_text_one)
|
|
||||||
2 -> getString(R.string.channel_subscriber_notification_instant_text_two)
|
|
||||||
3 -> getString(R.string.channel_subscriber_notification_instant_text_three)
|
|
||||||
4 -> getString(R.string.channel_subscriber_notification_instant_text_four)
|
|
||||||
5 -> getString(R.string.channel_subscriber_notification_instant_text_five)
|
|
||||||
6 -> getString(R.string.channel_subscriber_notification_instant_text_six)
|
|
||||||
else -> getString(R.string.channel_subscriber_notification_instant_text_more, instantSubscriptions.size)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when (instantSubscriptions.size) {
|
|
||||||
1 -> getString(R.string.channel_subscriber_notification_noinstant_text_one)
|
1 -> getString(R.string.channel_subscriber_notification_noinstant_text_one)
|
||||||
2 -> getString(R.string.channel_subscriber_notification_noinstant_text_two)
|
2 -> getString(R.string.channel_subscriber_notification_noinstant_text_two)
|
||||||
3 -> getString(R.string.channel_subscriber_notification_noinstant_text_three)
|
3 -> getString(R.string.channel_subscriber_notification_noinstant_text_three)
|
||||||
|
@ -241,7 +226,6 @@ class SubscriberService : Service() {
|
||||||
6 -> getString(R.string.channel_subscriber_notification_noinstant_text_six)
|
6 -> getString(R.string.channel_subscriber_notification_noinstant_text_six)
|
||||||
else -> getString(R.string.channel_subscriber_notification_noinstant_text_more, instantSubscriptions.size)
|
else -> getString(R.string.channel_subscriber_notification_noinstant_text_more, instantSubscriptions.size)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
serviceNotification = createNotification(title, text)
|
serviceNotification = createNotification(title, text)
|
||||||
notificationManager?.notify(NOTIFICATION_SERVICE_ID, serviceNotification)
|
notificationManager?.notify(NOTIFICATION_SERVICE_ID, serviceNotification)
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,10 +114,7 @@ class AddFragment : DialogFragment() {
|
||||||
// Set foreground description text
|
// Set foreground description text
|
||||||
subscribeForegroundDescription.text = getString(R.string.add_dialog_foreground_description, shortUrl(appBaseUrl))
|
subscribeForegroundDescription.text = getString(R.string.add_dialog_foreground_description, shortUrl(appBaseUrl))
|
||||||
|
|
||||||
// Show/hide based on flavor (faster shortcut for validateInputSubscribeView, which can only run onShow)
|
|
||||||
if (!BuildConfig.FIREBASE_AVAILABLE) {
|
|
||||||
subscribeInstantDeliveryBox.visibility = View.GONE
|
subscribeInstantDeliveryBox.visibility = View.GONE
|
||||||
}
|
|
||||||
|
|
||||||
// Add baseUrl auto-complete behavior
|
// Add baseUrl auto-complete behavior
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
@ -291,16 +288,6 @@ class AddFragment : DialogFragment() {
|
||||||
private fun validateInputSubscribeView() {
|
private fun validateInputSubscribeView() {
|
||||||
if (!this::positiveButton.isInitialized) return // As per crash seen in Google Play
|
if (!this::positiveButton.isInitialized) return // As per crash seen in Google Play
|
||||||
|
|
||||||
// Show/hide things: This logic is intentionally kept simple. Do not simplify "just because it's pretty".
|
|
||||||
val instantToggleAllowed = if (!BuildConfig.FIREBASE_AVAILABLE) {
|
|
||||||
false
|
|
||||||
} else if (subscribeUseAnotherServerCheckbox.isChecked && subscribeBaseUrlText.text.toString() == appBaseUrl) {
|
|
||||||
true
|
|
||||||
} else if (!subscribeUseAnotherServerCheckbox.isChecked && defaultBaseUrl == null) {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
if (subscribeUseAnotherServerCheckbox.isChecked) {
|
if (subscribeUseAnotherServerCheckbox.isChecked) {
|
||||||
subscribeUseAnotherServerDescription.visibility = View.VISIBLE
|
subscribeUseAnotherServerDescription.visibility = View.VISIBLE
|
||||||
subscribeBaseUrlLayout.visibility = View.VISIBLE
|
subscribeBaseUrlLayout.visibility = View.VISIBLE
|
||||||
|
@ -308,15 +295,9 @@ class AddFragment : DialogFragment() {
|
||||||
subscribeUseAnotherServerDescription.visibility = View.GONE
|
subscribeUseAnotherServerDescription.visibility = View.GONE
|
||||||
subscribeBaseUrlLayout.visibility = View.GONE
|
subscribeBaseUrlLayout.visibility = View.GONE
|
||||||
}
|
}
|
||||||
if (instantToggleAllowed) {
|
|
||||||
subscribeInstantDeliveryBox.visibility = View.VISIBLE
|
|
||||||
subscribeInstantDeliveryDescription.visibility = if (subscribeInstantDeliveryCheckbox.isChecked) View.VISIBLE else View.GONE
|
|
||||||
subscribeForegroundDescription.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
subscribeInstantDeliveryBox.visibility = View.GONE
|
subscribeInstantDeliveryBox.visibility = View.GONE
|
||||||
subscribeInstantDeliveryDescription.visibility = View.GONE
|
subscribeInstantDeliveryDescription.visibility = View.GONE
|
||||||
subscribeForegroundDescription.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
|
subscribeForegroundDescription.visibility = View.GONE
|
||||||
}
|
|
||||||
|
|
||||||
// Enable/disable "Subscribe" button
|
// Enable/disable "Subscribe" button
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
@ -356,7 +337,7 @@ class AddFragment : DialogFragment() {
|
||||||
activity.runOnUiThread {
|
activity.runOnUiThread {
|
||||||
val topic = subscribeTopicText.text.toString()
|
val topic = subscribeTopicText.text.toString()
|
||||||
val baseUrl = getBaseUrl()
|
val baseUrl = getBaseUrl()
|
||||||
val instant = !BuildConfig.FIREBASE_AVAILABLE || baseUrl != appBaseUrl || subscribeInstantDeliveryCheckbox.isChecked
|
val instant = baseUrl != appBaseUrl || subscribeInstantDeliveryCheckbox.isChecked
|
||||||
subscribeListener.onSubscribe(topic, baseUrl, instant)
|
subscribeListener.onSubscribe(topic, baseUrl, instant)
|
||||||
dialog?.dismiss()
|
dialog?.dismiss()
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,6 @@ import io.heckel.ntfy.app.Application
|
||||||
import io.heckel.ntfy.db.Notification
|
import io.heckel.ntfy.db.Notification
|
||||||
import io.heckel.ntfy.db.Repository
|
import io.heckel.ntfy.db.Repository
|
||||||
import io.heckel.ntfy.db.Subscription
|
import io.heckel.ntfy.db.Subscription
|
||||||
import io.heckel.ntfy.firebase.FirebaseMessenger
|
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
import io.heckel.ntfy.msg.ApiService
|
import io.heckel.ntfy.msg.ApiService
|
||||||
import io.heckel.ntfy.msg.NotificationService
|
import io.heckel.ntfy.msg.NotificationService
|
||||||
|
@ -47,7 +46,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
}
|
}
|
||||||
private val repository by lazy { (application as Application).repository }
|
private val repository by lazy { (application as Application).repository }
|
||||||
private val api = ApiService()
|
private val api = ApiService()
|
||||||
private val messenger = FirebaseMessenger()
|
|
||||||
private var notifier: NotificationService? = null // Context-dependent
|
private var notifier: NotificationService? = null // Context-dependent
|
||||||
private var appBaseUrl: String? = null // Context-dependent
|
private var appBaseUrl: String? = null // Context-dependent
|
||||||
|
|
||||||
|
@ -78,8 +76,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
notifier = NotificationService(this)
|
notifier = NotificationService(this)
|
||||||
appBaseUrl = getString(R.string.app_base_url)
|
appBaseUrl = getString(R.string.app_base_url)
|
||||||
|
|
||||||
// Show 'Back' button
|
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
|
|
||||||
// Handle direct deep links to topic "ntfy://..."
|
// Handle direct deep links to topic "ntfy://..."
|
||||||
val url = intent?.data
|
val url = intent?.data
|
||||||
|
@ -90,6 +87,10 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
finishAffinity()
|
||||||
|
}
|
||||||
|
|
||||||
private fun maybeSubscribeAndLoadView(url: Uri) {
|
private fun maybeSubscribeAndLoadView(url: Uri) {
|
||||||
if (url.pathSegments.size != 1) {
|
if (url.pathSegments.size != 1) {
|
||||||
Log.w(TAG, "Invalid link $url. Aborting.")
|
Log.w(TAG, "Invalid link $url. Aborting.")
|
||||||
|
@ -127,12 +128,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
)
|
)
|
||||||
repository.addSubscription(subscription)
|
repository.addSubscription(subscription)
|
||||||
|
|
||||||
// Subscribe to Firebase topic if ntfy.sh (even if instant, just to be sure!)
|
|
||||||
if (baseUrl == appBaseUrl) {
|
|
||||||
Log.d(TAG, "Subscribing to Firebase topic $topic")
|
|
||||||
messenger.subscribe(topic)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch cached messages
|
// Fetch cached messages
|
||||||
try {
|
try {
|
||||||
val user = repository.getUser(subscription.baseUrl) // May be null
|
val user = repository.getUser(subscription.baseUrl) // May be null
|
||||||
|
@ -176,24 +171,12 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
val topicUrl = topicShortUrl(subscriptionBaseUrl, subscriptionTopic)
|
val topicUrl = topicShortUrl(subscriptionBaseUrl, subscriptionTopic)
|
||||||
title = subscriptionDisplayName
|
title = subscriptionDisplayName
|
||||||
|
|
||||||
// Set "how to instructions"
|
|
||||||
val howToExample: TextView = findViewById(R.id.detail_how_to_example)
|
|
||||||
howToExample.linksClickable = true
|
|
||||||
|
|
||||||
val howToText = getString(R.string.detail_how_to_example, topicUrl)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
howToExample.text = Html.fromHtml(howToText, Html.FROM_HTML_MODE_LEGACY)
|
|
||||||
} else {
|
|
||||||
howToExample.text = Html.fromHtml(howToText)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swipe to refresh
|
// Swipe to refresh
|
||||||
mainListContainer = findViewById(R.id.detail_notification_list_container)
|
mainListContainer = findViewById(R.id.detail_notification_list_container)
|
||||||
mainListContainer.setOnRefreshListener { refresh() }
|
mainListContainer.setOnRefreshListener { refresh() }
|
||||||
mainListContainer.setColorSchemeResources(Colors.refreshProgressIndicator)
|
mainListContainer.setColorSchemeResources(Colors.refreshProgressIndicator)
|
||||||
|
|
||||||
// Update main list based on viewModel (& its datasource/livedata)
|
// Update main list based on viewModel (& its datasource/livedata)
|
||||||
val noEntriesText: View = findViewById(R.id.detail_no_notifications)
|
|
||||||
val onNotificationClick = { n: Notification -> onNotificationClick(n) }
|
val onNotificationClick = { n: Notification -> onNotificationClick(n) }
|
||||||
val onNotificationLongClick = { n: Notification -> onNotificationLongClick(n) }
|
val onNotificationLongClick = { n: Notification -> onNotificationLongClick(n) }
|
||||||
|
|
||||||
|
@ -207,10 +190,8 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
adapter.submitList(it as MutableList<Notification>)
|
adapter.submitList(it as MutableList<Notification>)
|
||||||
if (it.isEmpty()) {
|
if (it.isEmpty()) {
|
||||||
mainListContainer.visibility = View.GONE
|
mainListContainer.visibility = View.GONE
|
||||||
noEntriesText.visibility = View.VISIBLE
|
|
||||||
} else {
|
} else {
|
||||||
mainListContainer.visibility = View.VISIBLE
|
mainListContainer.visibility = View.VISIBLE
|
||||||
noEntriesText.visibility = View.GONE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel notifications that still have popups
|
// Cancel notifications that still have popups
|
||||||
|
@ -345,10 +326,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.detail_menu_test -> {
|
|
||||||
onTestClick()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.detail_menu_notifications_enabled -> {
|
R.id.detail_menu_notifications_enabled -> {
|
||||||
onMutedUntilClick(enable = false)
|
onMutedUntilClick(enable = false)
|
||||||
true
|
true
|
||||||
|
@ -369,22 +346,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
onInstantEnableClick(enable = false)
|
onInstantEnableClick(enable = false)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.detail_menu_copy_url -> {
|
|
||||||
onCopyUrlClick()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.detail_menu_clear -> {
|
|
||||||
onClearClick()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.detail_menu_settings -> {
|
|
||||||
onSettingsClick()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.detail_menu_unsubscribe -> {
|
|
||||||
onDeleteClick()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -529,16 +490,10 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
val appBaseUrl = getString(R.string.app_base_url)
|
val appBaseUrl = getString(R.string.app_base_url)
|
||||||
val enableInstantItem = menu.findItem(R.id.detail_menu_enable_instant)
|
val enableInstantItem = menu.findItem(R.id.detail_menu_enable_instant)
|
||||||
val disableInstantItem = menu.findItem(R.id.detail_menu_disable_instant)
|
val disableInstantItem = menu.findItem(R.id.detail_menu_disable_instant)
|
||||||
val allowToggleInstant = BuildConfig.FIREBASE_AVAILABLE && subscriptionBaseUrl == appBaseUrl
|
|
||||||
if (allowToggleInstant) {
|
|
||||||
enableInstantItem?.isVisible = !subscriptionInstant
|
|
||||||
disableInstantItem?.isVisible = subscriptionInstant
|
|
||||||
} else {
|
|
||||||
enableInstantItem?.isVisible = false
|
enableInstantItem?.isVisible = false
|
||||||
disableInstantItem?.isVisible = false
|
disableInstantItem?.isVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun showHideMutedUntilMenuItems(mutedUntilTimestamp: Long) {
|
private fun showHideMutedUntilMenuItems(mutedUntilTimestamp: Long) {
|
||||||
if (!this::menu.isInitialized) {
|
if (!this::menu.isInitialized) {
|
||||||
|
@ -608,9 +563,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
repository.removeAllNotifications(subscriptionId)
|
repository.removeAllNotifications(subscriptionId)
|
||||||
repository.removeSubscription(subscriptionId)
|
repository.removeSubscription(subscriptionId)
|
||||||
if (subscriptionBaseUrl == appBaseUrl) {
|
|
||||||
messenger.unsubscribe(subscriptionTopic)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,7 +114,6 @@ class DetailSettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private fun loadView() {
|
private fun loadView() {
|
||||||
if (subscription.upAppId == null) {
|
if (subscription.upAppId == null) {
|
||||||
loadInstantPref()
|
|
||||||
loadMutedUntilPref()
|
loadMutedUntilPref()
|
||||||
loadMinPriorityPref()
|
loadMinPriorityPref()
|
||||||
loadAutoDeletePref()
|
loadAutoDeletePref()
|
||||||
|
@ -134,29 +133,6 @@ class DetailSettingsActivity : AppCompatActivity() {
|
||||||
loadTopicUrlPref()
|
loadTopicUrlPref()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadInstantPref() {
|
|
||||||
val appBaseUrl = getString(R.string.app_base_url)
|
|
||||||
val prefId = context?.getString(R.string.detail_settings_notifications_instant_key) ?: return
|
|
||||||
val pref: SwitchPreference? = findPreference(prefId)
|
|
||||||
pref?.isVisible = BuildConfig.FIREBASE_AVAILABLE && subscription.baseUrl == appBaseUrl
|
|
||||||
pref?.isChecked = subscription.instant
|
|
||||||
pref?.preferenceDataStore = object : PreferenceDataStore() {
|
|
||||||
override fun putBoolean(key: String?, value: Boolean) {
|
|
||||||
save(subscription.copy(instant = value), refresh = true)
|
|
||||||
}
|
|
||||||
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
|
|
||||||
return subscription.instant
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pref?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { preference ->
|
|
||||||
if (preference.isChecked) {
|
|
||||||
getString(R.string.detail_settings_notifications_instant_summary_on)
|
|
||||||
} else {
|
|
||||||
getString(R.string.detail_settings_notifications_instant_summary_off)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadDedicatedChannelsPrefs() {
|
private fun loadDedicatedChannelsPrefs() {
|
||||||
val prefId = context?.getString(R.string.detail_settings_notifications_dedicated_channels_key) ?: return
|
val prefId = context?.getString(R.string.detail_settings_notifications_dedicated_channels_key) ?: return
|
||||||
val pref: SwitchPreference? = findPreference(prefId)
|
val pref: SwitchPreference? = findPreference(prefId)
|
||||||
|
|
|
@ -34,13 +34,13 @@ import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.app.Application
|
import io.heckel.ntfy.app.Application
|
||||||
import io.heckel.ntfy.db.Repository
|
import io.heckel.ntfy.db.Repository
|
||||||
import io.heckel.ntfy.db.Subscription
|
import io.heckel.ntfy.db.Subscription
|
||||||
import io.heckel.ntfy.firebase.FirebaseMessenger
|
|
||||||
import io.heckel.ntfy.msg.ApiService
|
import io.heckel.ntfy.msg.ApiService
|
||||||
import io.heckel.ntfy.msg.DownloadManager
|
import io.heckel.ntfy.msg.DownloadManager
|
||||||
import io.heckel.ntfy.msg.DownloadType
|
import io.heckel.ntfy.msg.DownloadType
|
||||||
import io.heckel.ntfy.msg.NotificationDispatcher
|
import io.heckel.ntfy.msg.NotificationDispatcher
|
||||||
import io.heckel.ntfy.service.SubscriberService
|
import io.heckel.ntfy.service.SubscriberService
|
||||||
import io.heckel.ntfy.service.SubscriberServiceManager
|
import io.heckel.ntfy.service.SubscriberServiceManager
|
||||||
|
import io.heckel.ntfy.up.TOPIC_MURENA
|
||||||
import io.heckel.ntfy.util.*
|
import io.heckel.ntfy.util.*
|
||||||
import io.heckel.ntfy.work.DeleteWorker
|
import io.heckel.ntfy.work.DeleteWorker
|
||||||
import io.heckel.ntfy.work.PollWorker
|
import io.heckel.ntfy.work.PollWorker
|
||||||
|
@ -57,14 +57,12 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
}
|
}
|
||||||
private val repository by lazy { (application as Application).repository }
|
private val repository by lazy { (application as Application).repository }
|
||||||
private val api = ApiService()
|
private val api = ApiService()
|
||||||
private val messenger = FirebaseMessenger()
|
|
||||||
|
|
||||||
// UI elements
|
// UI elements
|
||||||
private lateinit var menu: Menu
|
private lateinit var menu: Menu
|
||||||
private lateinit var mainList: RecyclerView
|
private lateinit var mainList: RecyclerView
|
||||||
private lateinit var mainListContainer: SwipeRefreshLayout
|
private lateinit var mainListContainer: SwipeRefreshLayout
|
||||||
private lateinit var adapter: MainAdapter
|
private lateinit var adapter: MainAdapter
|
||||||
private lateinit var fab: FloatingActionButton
|
|
||||||
|
|
||||||
// Other stuff
|
// Other stuff
|
||||||
private var actionMode: ActionMode? = null
|
private var actionMode: ActionMode? = null
|
||||||
|
@ -87,12 +85,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
// Action bar
|
// Action bar
|
||||||
title = getString(R.string.main_action_bar_title)
|
title = getString(R.string.main_action_bar_title)
|
||||||
|
|
||||||
// Floating action button ("+")
|
|
||||||
fab = findViewById(R.id.fab)
|
|
||||||
fab.setOnClickListener {
|
|
||||||
onSubscribeButtonClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swipe to refresh
|
// Swipe to refresh
|
||||||
mainListContainer = findViewById(R.id.main_subscriptions_list_container)
|
mainListContainer = findViewById(R.id.main_subscriptions_list_container)
|
||||||
mainListContainer.setOnRefreshListener { refreshAllSubscriptions() }
|
mainListContainer.setOnRefreshListener { refreshAllSubscriptions() }
|
||||||
|
@ -199,9 +191,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
// Create notification channels right away, so we can configure them immediately after installing the app
|
// Create notification channels right away, so we can configure them immediately after installing the app
|
||||||
dispatcher?.init()
|
dispatcher?.init()
|
||||||
|
|
||||||
// Subscribe to control Firebase channel (so we can re-start the foreground service if it dies)
|
|
||||||
messenger.subscribe(ApiService.CONTROL_TOPIC)
|
|
||||||
|
|
||||||
// Darrkkkk mode
|
// Darrkkkk mode
|
||||||
AppCompatDelegate.setDefaultNightMode(repository.getDarkMode())
|
AppCompatDelegate.setDefaultNightMode(repository.getDarkMode())
|
||||||
|
|
||||||
|
@ -212,6 +201,42 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
maybeRequestNotificationPermission()
|
maybeRequestNotificationPermission()
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val subscriptions = repository.getSubscriptions()
|
||||||
|
val defaultTopic = TOPIC_MURENA
|
||||||
|
val hasTestTopic = subscriptions.any { it.topic == defaultTopic }
|
||||||
|
if (hasTestTopic) {
|
||||||
|
showMurenaSub()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val subscription = Subscription(
|
||||||
|
id = randomSubscriptionId(),
|
||||||
|
baseUrl = appBaseUrl!!,
|
||||||
|
topic = defaultTopic,
|
||||||
|
instant = true,
|
||||||
|
dedicatedChannels = false,
|
||||||
|
mutedUntil = Repository.MUTED_UNTIL_FOREVER,
|
||||||
|
minPriority = Repository.MIN_PRIORITY_USE_GLOBAL,
|
||||||
|
autoDelete = Repository.AUTO_DELETE_USE_GLOBAL,
|
||||||
|
insistent = Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL,
|
||||||
|
lastNotificationId = null,
|
||||||
|
icon = null,
|
||||||
|
upAppId = null,
|
||||||
|
upConnectorToken = null,
|
||||||
|
displayName = null,
|
||||||
|
totalCount = 0,
|
||||||
|
newCount = 0,
|
||||||
|
lastActive = Date().time / 1000
|
||||||
|
)
|
||||||
|
viewModel.add(subscription)
|
||||||
|
showMurenaSub()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun showMurenaSub() {
|
||||||
|
startDetailView(repository.getSubscriptions().find { it.topic == "murena_notification" }!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeRequestNotificationPermission() {
|
private fun maybeRequestNotificationPermission() {
|
||||||
|
@ -349,10 +374,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
}
|
}
|
||||||
val mutedUntilSeconds = repository.getGlobalMutedUntil()
|
val mutedUntilSeconds = repository.getGlobalMutedUntil()
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
// Show/hide in-app rate widget
|
|
||||||
val rateAppItem = menu.findItem(R.id.main_menu_rate)
|
|
||||||
rateAppItem.isVisible = BuildConfig.RATE_APP_AVAILABLE
|
|
||||||
|
|
||||||
// Pause notification icons
|
// Pause notification icons
|
||||||
val notificationsEnabledItem = menu.findItem(R.id.main_menu_notifications_enabled)
|
val notificationsEnabledItem = menu.findItem(R.id.main_menu_notifications_enabled)
|
||||||
val notificationsDisabledUntilItem = menu.findItem(R.id.main_menu_notifications_disabled_until)
|
val notificationsDisabledUntilItem = menu.findItem(R.id.main_menu_notifications_disabled_until)
|
||||||
|
@ -381,30 +402,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
onNotificationSettingsClick(enable = true)
|
onNotificationSettingsClick(enable = true)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.main_menu_settings -> {
|
|
||||||
startActivity(Intent(this, SettingsActivity::class.java))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.main_menu_report_bug -> {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_report_bug_url))))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.main_menu_rate -> {
|
|
||||||
try {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName")))
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$packageName")))
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.main_menu_donate -> {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_donate_url))))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.main_menu_docs -> {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.main_menu_docs_url))))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -466,12 +463,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
)
|
)
|
||||||
viewModel.add(subscription)
|
viewModel.add(subscription)
|
||||||
|
|
||||||
// Subscribe to Firebase topic if ntfy.sh (even if instant, just to be sure!)
|
|
||||||
if (baseUrl == appBaseUrl) {
|
|
||||||
Log.d(TAG, "Subscribing to Firebase topic $topic")
|
|
||||||
messenger.subscribe(topic)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch cached messages
|
// Fetch cached messages
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
@ -635,18 +626,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
actionMode = startActionMode(this)
|
actionMode = startActionMode(this)
|
||||||
adapter.toggleSelection(subscription.id)
|
adapter.toggleSelection(subscription.id)
|
||||||
|
|
||||||
// Fade out FAB
|
|
||||||
fab.alpha = 1f
|
|
||||||
fab
|
|
||||||
.animate()
|
|
||||||
.alpha(0f)
|
|
||||||
.setDuration(ANIMATION_DURATION)
|
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
|
||||||
fab.visibility = View.GONE
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Fade status bar color
|
// Fade status bar color
|
||||||
val fromColor = ContextCompat.getColor(this, Colors.statusBarNormal(this))
|
val fromColor = ContextCompat.getColor(this, Colors.statusBarNormal(this))
|
||||||
val toColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this))
|
val toColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this))
|
||||||
|
@ -663,19 +642,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
adapter.selected.clear()
|
adapter.selected.clear()
|
||||||
redrawList()
|
redrawList()
|
||||||
|
|
||||||
// Fade in FAB
|
|
||||||
fab.alpha = 0f
|
|
||||||
fab.visibility = View.VISIBLE
|
|
||||||
fab
|
|
||||||
.animate()
|
|
||||||
.alpha(1f)
|
|
||||||
.setDuration(ANIMATION_DURATION)
|
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
|
||||||
fab.visibility = View.VISIBLE // Required to replace the old listener
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Fade status bar color
|
// Fade status bar color
|
||||||
val fromColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this))
|
val fromColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this))
|
||||||
val toColor = ContextCompat.getColor(this, Colors.statusBarNormal(this))
|
val toColor = ContextCompat.getColor(this, Colors.statusBarNormal(this))
|
||||||
|
|
|
@ -105,7 +105,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
|
||||||
dateView.visibility = if (isUnifiedPush) View.GONE else View.VISIBLE
|
dateView.visibility = if (isUnifiedPush) View.GONE else View.VISIBLE
|
||||||
notificationDisabledUntilImageView.visibility = if (showMutedUntilIcon) View.VISIBLE else View.GONE
|
notificationDisabledUntilImageView.visibility = if (showMutedUntilIcon) View.VISIBLE else View.GONE
|
||||||
notificationDisabledForeverImageView.visibility = if (showMutedForeverIcon) View.VISIBLE else View.GONE
|
notificationDisabledForeverImageView.visibility = if (showMutedForeverIcon) View.VISIBLE else View.GONE
|
||||||
instantImageView.visibility = if (subscription.instant && BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
|
instantImageView.visibility = View.GONE
|
||||||
if (isUnifiedPush || subscription.newCount == 0) {
|
if (isUnifiedPush || subscription.newCount == 0) {
|
||||||
newItemsView.visibility = View.GONE
|
newItemsView.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -68,15 +68,23 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
|
||||||
|
|
||||||
// Add subscription
|
// Add subscription
|
||||||
val baseUrl = repository.getDefaultBaseUrl() ?: context.getString(R.string.app_base_url)
|
val baseUrl = repository.getDefaultBaseUrl() ?: context.getString(R.string.app_base_url)
|
||||||
val topic = UP_PREFIX + randomString(TOPIC_RANDOM_ID_LENGTH)
|
var topic = UP_PREFIX + randomString(TOPIC_RANDOM_ID_LENGTH)
|
||||||
|
if (appId == PACKAGE_MURENA_UNIFIED_PUSH) {
|
||||||
|
topic = TOPIC_MURENA
|
||||||
|
}
|
||||||
val endpoint = topicUrlUp(baseUrl, topic)
|
val endpoint = topicUrlUp(baseUrl, topic)
|
||||||
|
val subscriptionId =
|
||||||
|
appId.takeIf { it == PACKAGE_MURENA_UNIFIED_PUSH }?.run {
|
||||||
|
repository.getSubscriptions()
|
||||||
|
.find { it.baseUrl == baseUrl && it.topic == topic }?.id
|
||||||
|
} ?: randomSubscriptionId()
|
||||||
val subscription = Subscription(
|
val subscription = Subscription(
|
||||||
id = randomSubscriptionId(),
|
id = subscriptionId,
|
||||||
baseUrl = baseUrl,
|
baseUrl = baseUrl,
|
||||||
topic = topic,
|
topic = topic,
|
||||||
instant = true, // No Firebase, always instant!
|
instant = true, // No Firebase, always instant!
|
||||||
dedicatedChannels = false,
|
dedicatedChannels = false,
|
||||||
mutedUntil = 0,
|
mutedUntil = Repository.MUTED_UNTIL_FOREVER,
|
||||||
minPriority = Repository.MIN_PRIORITY_USE_GLOBAL,
|
minPriority = Repository.MIN_PRIORITY_USE_GLOBAL,
|
||||||
autoDelete = Repository.AUTO_DELETE_USE_GLOBAL,
|
autoDelete = Repository.AUTO_DELETE_USE_GLOBAL,
|
||||||
insistent = Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL,
|
insistent = Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL,
|
||||||
|
@ -89,10 +97,15 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
|
||||||
newCount = 0,
|
newCount = 0,
|
||||||
lastActive = Date().time/1000
|
lastActive = Date().time/1000
|
||||||
)
|
)
|
||||||
Log.d(TAG, "Adding subscription with for app $appId (connectorToken $connectorToken): $subscription")
|
|
||||||
try {
|
try {
|
||||||
// Note, this may fail due to a SQL constraint exception, see https://github.com/binwiederhier/ntfy/issues/185
|
// Note, this may fail due to a SQL constraint exception, see https://github.com/binwiederhier/ntfy/issues/185
|
||||||
|
if (appId == PACKAGE_MURENA_UNIFIED_PUSH) {
|
||||||
|
Log.d(TAG, "Updating subscription with for app $appId (connectorToken $connectorToken): $subscription")
|
||||||
|
repository.updateSubscription(subscription)
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Adding subscription with for app $appId (connectorToken $connectorToken): $subscription")
|
||||||
repository.addSubscription(subscription)
|
repository.addSubscription(subscription)
|
||||||
|
}
|
||||||
distributor.sendEndpoint(appId, connectorToken, endpoint)
|
distributor.sendEndpoint(appId, connectorToken, endpoint)
|
||||||
|
|
||||||
// Refresh (and maybe start) foreground service
|
// Refresh (and maybe start) foreground service
|
||||||
|
|
|
@ -19,4 +19,9 @@ const val EXTRA_APPLICATION = "application"
|
||||||
const val EXTRA_TOKEN = "token"
|
const val EXTRA_TOKEN = "token"
|
||||||
const val EXTRA_ENDPOINT = "endpoint"
|
const val EXTRA_ENDPOINT = "endpoint"
|
||||||
const val EXTRA_MESSAGE = "message"
|
const val EXTRA_MESSAGE = "message"
|
||||||
|
const val EXTRA_TITLE = "title"
|
||||||
const val EXTRA_BYTES_MESSAGE = "bytesMessage"
|
const val EXTRA_BYTES_MESSAGE = "bytesMessage"
|
||||||
|
const val EXTRA_BYTES_TITLE = "bytesTitle"
|
||||||
|
|
||||||
|
const val PACKAGE_MURENA_UNIFIED_PUSH = "foundation.e.unifiedpoc"
|
||||||
|
const val TOPIC_MURENA = "murena_notification"
|
||||||
|
|
|
@ -9,14 +9,16 @@ import io.heckel.ntfy.util.Log
|
||||||
* See https://unifiedpush.org/spec/android/ for details.
|
* See https://unifiedpush.org/spec/android/ for details.
|
||||||
*/
|
*/
|
||||||
class Distributor(val context: Context) {
|
class Distributor(val context: Context) {
|
||||||
fun sendMessage(app: String, connectorToken: String, message: ByteArray) {
|
fun sendMessage(app: String, connectorToken: String, message: ByteArray, title: ByteArray) {
|
||||||
Log.d(TAG, "Sending MESSAGE to $app (token=$connectorToken): ${message.size} bytes")
|
Log.d(TAG, "Sending MESSAGE to $app (token=$connectorToken): ${message.size} bytes")
|
||||||
val broadcastIntent = Intent()
|
val broadcastIntent = Intent()
|
||||||
broadcastIntent.`package` = app
|
broadcastIntent.`package` = app
|
||||||
broadcastIntent.action = ACTION_MESSAGE
|
broadcastIntent.action = ACTION_MESSAGE
|
||||||
broadcastIntent.putExtra(EXTRA_TOKEN, connectorToken)
|
broadcastIntent.putExtra(EXTRA_TOKEN, connectorToken)
|
||||||
broadcastIntent.putExtra(EXTRA_MESSAGE, String(message)) // UTF-8
|
broadcastIntent.putExtra(EXTRA_MESSAGE, String(message)) // UTF-8
|
||||||
|
broadcastIntent.putExtra(EXTRA_TITLE, String(title)) // UTF-8
|
||||||
broadcastIntent.putExtra(EXTRA_BYTES_MESSAGE, message)
|
broadcastIntent.putExtra(EXTRA_BYTES_MESSAGE, message)
|
||||||
|
broadcastIntent.putExtra(EXTRA_BYTES_TITLE, title)
|
||||||
context.sendBroadcast(broadcastIntent)
|
context.sendBroadcast(broadcastIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -170,18 +170,26 @@ fun decodeMessage(notification: Notification): String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decodeBytesMessage(notification: Notification): ByteArray {
|
fun decodeBytes(notification: Notification, string: String): ByteArray {
|
||||||
return try {
|
return try {
|
||||||
if (notification.encoding == MESSAGE_ENCODING_BASE64) {
|
if (notification.encoding == MESSAGE_ENCODING_BASE64) {
|
||||||
Base64.decode(notification.message, Base64.DEFAULT)
|
Base64.decode(string, Base64.DEFAULT)
|
||||||
} else {
|
} else {
|
||||||
notification.message.toByteArray()
|
string.toByteArray()
|
||||||
}
|
}
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
notification.message.toByteArray()
|
string.toByteArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun decodeBytesMessage(notification: Notification): ByteArray {
|
||||||
|
return decodeBytes(notification, notification.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeBytesTitle(notification: Notification): ByteArray {
|
||||||
|
return decodeBytes(notification, notification.title)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See above; prepend emojis to title if the title is non-empty.
|
* See above; prepend emojis to title if the title is non-empty.
|
||||||
* Otherwise, they are prepended to the message.
|
* Otherwise, they are prepended to the message.
|
||||||
|
|
74
app/src/main/res/drawable/ic_murena_background.xml
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector
|
||||||
|
android:height="108dp"
|
||||||
|
android:width="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
</vector>
|
|
@ -26,51 +26,4 @@
|
||||||
app:layoutManager="LinearLayoutManager"/>
|
app:layoutManager="LinearLayoutManager"/>
|
||||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/detail_no_notifications" app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent">
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" app:srcCompat="@drawable/ic_sms_gray_48dp"
|
|
||||||
android:id="@+id/detail_no_notifications_image"/>
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/detail_no_notifications_text"
|
|
||||||
android:text="@string/detail_no_notifications_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
|
||||||
android:padding="10dp" android:gravity="center_horizontal"
|
|
||||||
android:paddingStart="50dp" android:paddingEnd="50dp"/>
|
|
||||||
<TextView
|
|
||||||
android:text="@string/detail_how_to_intro"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/detail_how_to_intro"
|
|
||||||
android:layout_marginTop="20dp"
|
|
||||||
android:layout_marginStart="50dp"
|
|
||||||
android:layout_marginEnd="50dp"/>
|
|
||||||
<TextView
|
|
||||||
android:text="@string/detail_how_to_example"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/detail_how_to_example"
|
|
||||||
android:layout_marginTop="7dp"
|
|
||||||
android:layout_marginStart="50dp"
|
|
||||||
android:layout_marginEnd="50dp"/>
|
|
||||||
<TextView
|
|
||||||
android:text="@string/detail_how_to_link"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:id="@+id/detail_how_to_link"
|
|
||||||
android:layout_marginTop="7dp"
|
|
||||||
android:layout_marginStart="50dp"
|
|
||||||
android:layout_marginEnd="50dp"
|
|
||||||
android:linksClickable="true"
|
|
||||||
android:autoLink="web"/>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -174,7 +174,8 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent"
|
android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/fab" app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:id="@+id/main_no_subscriptions" android:visibility="gone">
|
android:id="@+id/main_no_subscriptions" android:visibility="gone">
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -208,16 +209,4 @@
|
||||||
android:autoLink="web"/>
|
android:autoLink="web"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/fab"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="24dp"
|
|
||||||
android:contentDescription="@string/main_add_button_description"
|
|
||||||
android:src="@drawable/ic_add_black_24dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
style="@style/FloatingActionButton"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -9,9 +9,4 @@
|
||||||
app:showAsAction="ifRoom" android:icon="@drawable/ic_bolt_outline_white_24dp"/>
|
app:showAsAction="ifRoom" android:icon="@drawable/ic_bolt_outline_white_24dp"/>
|
||||||
<item android:id="@+id/detail_menu_disable_instant" android:title="@string/detail_menu_disable_instant"
|
<item android:id="@+id/detail_menu_disable_instant" android:title="@string/detail_menu_disable_instant"
|
||||||
android:icon="@drawable/ic_bolt_white_24dp" app:showAsAction="ifRoom"/>
|
android:icon="@drawable/ic_bolt_white_24dp" app:showAsAction="ifRoom"/>
|
||||||
<item android:id="@+id/detail_menu_settings" android:title="@string/detail_menu_settings"/>
|
|
||||||
<item android:id="@+id/detail_menu_copy_url" android:title="@string/detail_menu_copy_url"/>
|
|
||||||
<item android:id="@+id/detail_menu_clear" android:title="@string/detail_menu_clear"/>
|
|
||||||
<item android:id="@+id/detail_menu_test" android:title="@string/detail_menu_test"/>
|
|
||||||
<item android:id="@+id/detail_menu_unsubscribe" android:title="@string/detail_menu_unsubscribe"/>
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|
|
@ -5,9 +5,4 @@
|
||||||
app:showAsAction="ifRoom" android:icon="@drawable/ic_notifications_off_time_white_outline_24dp"/>
|
app:showAsAction="ifRoom" android:icon="@drawable/ic_notifications_off_time_white_outline_24dp"/>
|
||||||
<item android:id="@+id/main_menu_notifications_disabled_forever" android:title="@string/detail_menu_notifications_disabled_forever"
|
<item android:id="@+id/main_menu_notifications_disabled_forever" android:title="@string/detail_menu_notifications_disabled_forever"
|
||||||
app:showAsAction="ifRoom" android:icon="@drawable/ic_notifications_off_white_outline_24dp"/>
|
app:showAsAction="ifRoom" android:icon="@drawable/ic_notifications_off_white_outline_24dp"/>
|
||||||
<item android:id="@+id/main_menu_settings" android:title="@string/main_menu_settings_title"/>
|
|
||||||
<item android:id="@+id/main_menu_docs" android:title="@string/main_menu_docs_title"/>
|
|
||||||
<item android:id="@+id/main_menu_rate" android:title="@string/main_menu_rate_title"/>
|
|
||||||
<item android:id="@+id/main_menu_donate" android:title="@string/main_menu_donate_title"/>
|
|
||||||
<item android:id="@+id/main_menu_report_bug" android:title="@string/main_menu_report_bug_title"/>
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|
5
app/src/main/res/mipmap-anydpi-v26/ic_murena.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_murena_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_murena_foreground"/>
|
||||||
|
</adaptive-icon>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_murena_round.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_murena_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_murena_foreground"/>
|
||||||
|
</adaptive-icon>
|
BIN
app/src/main/res/mipmap-hdpi/ic_murena.webp
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_murena_foreground.webp
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_murena_round.webp
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_murena.webp
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_murena_foreground.webp
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_murena_round.webp
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_murena.webp
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_murena_foreground.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_murena_round.webp
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_murena.webp
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_murena_foreground.webp
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_murena_round.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_murena.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_murena_foreground.webp
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_murena_round.webp
Normal file
After Width: | Height: | Size: 18 KiB |
|
@ -10,12 +10,12 @@
|
||||||
- https://developer.android.com/guide/topics/ui/look-and-feel/themes
|
- https://developer.android.com/guide/topics/ui/look-and-feel/themes
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
<item name="colorPrimary">@color/teal_light</item>
|
<item name="colorPrimary">@color/e_accent_dark</item>
|
||||||
<item name="colorAccent">@color/teal_light</item> <!-- checkboxes, text fields -->
|
<item name="colorAccent">@color/e_accent_dark</item> <!-- checkboxes, text fields -->
|
||||||
<item name="android:colorBackground">@color/black_900</item> <!-- background -->
|
<item name="android:colorBackground">@color/black_900</item> <!-- background -->
|
||||||
<item name="android:statusBarColor">@color/black_900</item>
|
<item name="android:statusBarColor">@color/e_accent_dark</item>
|
||||||
<item name="actionModeBackground">@color/black_900</item>
|
<item name="actionModeBackground">@color/e_accent_dark</item>
|
||||||
|
|
||||||
<!-- Action bar background & text color -->
|
<!-- Action bar background & text color -->
|
||||||
<item name="colorSurface">@color/black_800b</item>
|
<item name="colorSurface">@color/black_800b</item>
|
||||||
|
|
|
@ -13,5 +13,7 @@
|
||||||
<color name="teal_dark">#2a6e60</color> <!-- Action bar background in action mode (light mode) -->
|
<color name="teal_dark">#2a6e60</color> <!-- Action bar background in action mode (light mode) -->
|
||||||
<color name="red_light">#fe4d2e</color> <!-- Danger text (dark mode) -->
|
<color name="red_light">#fe4d2e</color> <!-- Danger text (dark mode) -->
|
||||||
<color name="red_dark">#c30000</color> <!-- Danger text (light mode) -->
|
<color name="red_dark">#c30000</color> <!-- Danger text (light mode) -->
|
||||||
|
<color name="e_accent_light">#0086FF</color>
|
||||||
|
<color name="e_accent_dark">#5DB2FF</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
||||||
|
|
4
app/src/main/res/values/ic_murena_background.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_murena_background">#FFFFFF</color>
|
||||||
|
</resources>
|
|
@ -1,11 +1,11 @@
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Main app theme; dark theme styles see values-night/styles.xml -->
|
<!-- Main app theme; dark theme styles see values-night/styles.xml -->
|
||||||
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
<item name="colorPrimary">@color/teal</item>
|
<item name="colorPrimary">@color/e_accent_light</item>
|
||||||
<item name="colorAccent">@color/teal</item> <!-- checkboxes, text fields -->
|
<item name="colorAccent">@color/e_accent_light</item> <!-- checkboxes, text fields -->
|
||||||
<item name="android:colorBackground">@color/white</item> <!-- background -->
|
<item name="android:colorBackground">@color/white</item> <!-- background -->
|
||||||
<item name="android:statusBarColor">@color/teal</item>
|
<item name="android:statusBarColor">@color/e_accent_light</item>
|
||||||
<item name="actionModeBackground">@color/teal_dark</item>
|
<item name="actionModeBackground">@color/e_accent_light</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="DangerText" parent="@android:style/TextAppearance">
|
<style name="DangerText" parent="@android:style/TextAppearance">
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
The translatable="false" attribute is just an additional safety. -->
|
The translatable="false" attribute is just an additional safety. -->
|
||||||
|
|
||||||
<!-- Main app constants -->
|
<!-- Main app constants -->
|
||||||
<string name="app_name" translatable="false">ntfy</string>
|
<string name="app_name" translatable="false">Murena Box</string>
|
||||||
<string name="app_base_url" translatable="false">https://ntfy.sh</string> <!-- If changed, you must also change google-services.json! -->
|
<string name="app_base_url" translatable="false">https://ntfy.sh</string> <!-- If changed, you must also change google-services.json! -->
|
||||||
|
|
||||||
<!-- Main activity -->
|
<!-- Main activity -->
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
app:entryValues="@array/settings_general_dark_mode_values"
|
app:entryValues="@array/settings_general_dark_mode_values"
|
||||||
app:defaultValue="-1"/>
|
app:defaultValue="-1"/>
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
<PreferenceCategory app:title="@string/settings_backup_restore_header">
|
<PreferenceCategory app:title="@string/settings_backup_restore_header" app:isPreferenceVisible="false">
|
||||||
<ListPreference
|
<ListPreference
|
||||||
app:key="@string/settings_backup_restore_backup_key"
|
app:key="@string/settings_backup_restore_backup_key"
|
||||||
app:title="@string/settings_backup_restore_backup_title"
|
app:title="@string/settings_backup_restore_backup_title"
|
||||||
|
@ -96,7 +96,7 @@
|
||||||
app:title="@string/settings_advanced_clear_logs_title"
|
app:title="@string/settings_advanced_clear_logs_title"
|
||||||
app:summary="@string/settings_advanced_clear_logs_summary"/>
|
app:summary="@string/settings_advanced_clear_logs_summary"/>
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
<PreferenceCategory app:title="@string/settings_about_header">
|
<PreferenceCategory app:title="@string/settings_about_header" app:isPreferenceVisible="false">
|
||||||
<Preference
|
<Preference
|
||||||
app:key="@string/settings_about_version_key"
|
app:key="@string/settings_about_version_key"
|
||||||
app:title="@string/settings_about_version_title"/>
|
app:title="@string/settings_about_version_title"/>
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
package io.heckel.ntfy.firebase
|
|
||||||
|
|
||||||
import com.google.firebase.messaging.FirebaseMessaging
|
|
||||||
import io.heckel.ntfy.util.Log
|
|
||||||
|
|
||||||
class FirebaseMessenger {
|
|
||||||
fun subscribe(topic: String) {
|
|
||||||
val firebase = maybeInstance() ?: return
|
|
||||||
firebase
|
|
||||||
.subscribeToTopic(topic)
|
|
||||||
.addOnCompleteListener {
|
|
||||||
Log.d(TAG, "Subscribing to topic $topic complete: result=${it.result}, exception=${it.exception}, successful=${it.isSuccessful}")
|
|
||||||
}
|
|
||||||
.addOnFailureListener { e ->
|
|
||||||
Log.e(TAG, "Subscribing to topic $topic failed: ${e.message}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unsubscribe(topic: String) {
|
|
||||||
val firebase = maybeInstance() ?: return
|
|
||||||
firebase.unsubscribeFromTopic(topic)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun maybeInstance(): FirebaseMessaging? {
|
|
||||||
return try {
|
|
||||||
FirebaseMessaging.getInstance()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Firebase instance unavailable: ${e.message}", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "NtfyFirebase"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,166 +0,0 @@
|
||||||
package io.heckel.ntfy.firebase
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.work.*
|
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService
|
|
||||||
import com.google.firebase.messaging.RemoteMessage
|
|
||||||
import io.heckel.ntfy.R
|
|
||||||
import io.heckel.ntfy.app.Application
|
|
||||||
import io.heckel.ntfy.db.Attachment
|
|
||||||
import io.heckel.ntfy.db.Icon
|
|
||||||
import io.heckel.ntfy.db.Notification
|
|
||||||
import io.heckel.ntfy.util.Log
|
|
||||||
import io.heckel.ntfy.msg.ApiService
|
|
||||||
import io.heckel.ntfy.msg.NotificationDispatcher
|
|
||||||
import io.heckel.ntfy.msg.NotificationParser
|
|
||||||
import io.heckel.ntfy.service.SubscriberService
|
|
||||||
import io.heckel.ntfy.util.nullIfZero
|
|
||||||
import io.heckel.ntfy.util.toPriority
|
|
||||||
import io.heckel.ntfy.util.topicShortUrl
|
|
||||||
import io.heckel.ntfy.work.PollWorker
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
class FirebaseService : FirebaseMessagingService() {
|
|
||||||
private val repository by lazy { (application as Application).repository }
|
|
||||||
private val dispatcher by lazy { NotificationDispatcher(this, repository) }
|
|
||||||
private val job = SupervisorJob()
|
|
||||||
private val messenger = FirebaseMessenger()
|
|
||||||
private val parser = NotificationParser()
|
|
||||||
|
|
||||||
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
|
||||||
// Init log (this is done in all entrypoints)
|
|
||||||
Log.init(this)
|
|
||||||
|
|
||||||
// We only process data messages
|
|
||||||
if (remoteMessage.data.isEmpty()) {
|
|
||||||
Log.d(TAG, "Discarding unexpected message (1): from=${remoteMessage.from}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch event
|
|
||||||
val data = remoteMessage.data
|
|
||||||
when (data["event"]) {
|
|
||||||
ApiService.EVENT_MESSAGE -> handleMessage(remoteMessage)
|
|
||||||
ApiService.EVENT_KEEPALIVE -> handleKeepalive(remoteMessage)
|
|
||||||
ApiService.EVENT_POLL_REQUEST -> handlePollRequest(remoteMessage)
|
|
||||||
else -> Log.d(TAG, "Discarding unexpected message (2): from=${remoteMessage.from}, data=${data}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleKeepalive(remoteMessage: RemoteMessage) {
|
|
||||||
Log.d(TAG, "Keepalive received, sending auto restart broadcast for foregrounds service")
|
|
||||||
sendBroadcast(Intent(this, SubscriberService.AutoRestartReceiver::class.java)) // Restart it if necessary!
|
|
||||||
val topic = remoteMessage.data["topic"]
|
|
||||||
if (topic != ApiService.CONTROL_TOPIC) {
|
|
||||||
Log.d(TAG, "Keepalive on non-control topic $topic received, subscribing to control topic ${ApiService.CONTROL_TOPIC}")
|
|
||||||
messenger.subscribe(ApiService.CONTROL_TOPIC)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handlePollRequest(remoteMessage: RemoteMessage) {
|
|
||||||
val baseUrl = getString(R.string.app_base_url) // Everything from Firebase comes from main service URL!
|
|
||||||
val topic = remoteMessage.data["topic"] ?: return
|
|
||||||
val constraints = Constraints.Builder()
|
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
||||||
.build()
|
|
||||||
val workName = "${PollWorker.WORK_NAME_ONCE_SINGE_PREFIX}_${baseUrl}_${topic}"
|
|
||||||
val workManager = WorkManager.getInstance(this)
|
|
||||||
val workRequest = OneTimeWorkRequest.Builder(PollWorker::class.java)
|
|
||||||
.setInputData(workDataOf(
|
|
||||||
PollWorker.INPUT_DATA_BASE_URL to baseUrl,
|
|
||||||
PollWorker.INPUT_DATA_TOPIC to topic
|
|
||||||
))
|
|
||||||
.setConstraints(constraints)
|
|
||||||
.build()
|
|
||||||
Log.d(TAG, "Poll request for ${topicShortUrl(baseUrl, topic)} received, scheduling unique poll worker with name $workName")
|
|
||||||
|
|
||||||
workManager.enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleMessage(remoteMessage: RemoteMessage) {
|
|
||||||
val data = remoteMessage.data
|
|
||||||
val id = data["id"]
|
|
||||||
val timestamp = data["time"]?.toLongOrNull()
|
|
||||||
val topic = data["topic"]
|
|
||||||
val title = data["title"]
|
|
||||||
val message = data["message"]
|
|
||||||
val priority = data["priority"]?.toIntOrNull()
|
|
||||||
val tags = data["tags"]
|
|
||||||
val click = data["click"]
|
|
||||||
val iconUrl = data["icon"]
|
|
||||||
val actions = data["actions"] // JSON array as string, sigh ...
|
|
||||||
val encoding = data["encoding"]
|
|
||||||
val attachmentName = data["attachment_name"] ?: "attachment.bin"
|
|
||||||
val attachmentType = data["attachment_type"]
|
|
||||||
val attachmentSize = data["attachment_size"]?.toLongOrNull()?.nullIfZero()
|
|
||||||
val attachmentExpires = data["attachment_expires"]?.toLongOrNull()?.nullIfZero()
|
|
||||||
val attachmentUrl = data["attachment_url"]
|
|
||||||
val truncated = (data["truncated"] ?: "") == "1"
|
|
||||||
if (id == null || topic == null || message == null || timestamp == null) {
|
|
||||||
Log.d(TAG, "Discarding unexpected message: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Log.d(TAG, "Received message: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}")
|
|
||||||
|
|
||||||
CoroutineScope(job).launch {
|
|
||||||
val baseUrl = getString(R.string.app_base_url) // Everything from Firebase comes from main service URL!
|
|
||||||
|
|
||||||
// Check if notification was truncated and discard if it will (or likely already did) arrive via instant delivery
|
|
||||||
val subscription = repository.getSubscription(baseUrl, topic) ?: return@launch
|
|
||||||
if (truncated && subscription.instant) {
|
|
||||||
Log.d(TAG, "Discarding truncated message that did/will arrive via instant delivery: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}")
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add notification
|
|
||||||
val attachment = if (attachmentUrl != null) {
|
|
||||||
Attachment(
|
|
||||||
name = attachmentName,
|
|
||||||
type = attachmentType,
|
|
||||||
size = attachmentSize,
|
|
||||||
expires = attachmentExpires,
|
|
||||||
url = attachmentUrl,
|
|
||||||
)
|
|
||||||
} else null
|
|
||||||
val icon: Icon? = if (iconUrl != null && iconUrl != "") Icon(url = iconUrl) else null
|
|
||||||
val notification = Notification(
|
|
||||||
id = id,
|
|
||||||
subscriptionId = subscription.id,
|
|
||||||
timestamp = timestamp,
|
|
||||||
title = title ?: "",
|
|
||||||
message = message,
|
|
||||||
encoding = encoding ?: "",
|
|
||||||
priority = toPriority(priority),
|
|
||||||
tags = tags ?: "",
|
|
||||||
click = click ?: "",
|
|
||||||
icon = icon,
|
|
||||||
actions = parser.parseActions(actions),
|
|
||||||
attachment = attachment,
|
|
||||||
notificationId = Random.nextInt(),
|
|
||||||
deleted = false
|
|
||||||
)
|
|
||||||
if (repository.addNotification(notification)) {
|
|
||||||
Log.d(TAG, "Dispatching notification: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}")
|
|
||||||
dispatcher.dispatch(subscription, notification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewToken(token: String) {
|
|
||||||
// Called if the FCM registration token is updated
|
|
||||||
// We don't actually use or care about the token, since we're using topics
|
|
||||||
Log.d(TAG, "Registration token was updated: $token")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "NtfyFirebase"
|
|
||||||
}
|
|
||||||
}
|
|