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"
|
||||
productFlavors {
|
||||
play {
|
||||
buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'true'
|
||||
buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'true'
|
||||
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'false'
|
||||
}
|
||||
fdroid {
|
||||
buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'false'
|
||||
buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'false'
|
||||
buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'true'
|
||||
}
|
||||
}
|
||||
|
@ -110,9 +103,6 @@ dependencies {
|
|||
// OkHttp (HTTP library)
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
||||
|
||||
// Firebase, sigh ... (only Google Play)
|
||||
playImplementation 'com.google.firebase:firebase-messaging:23.1.2'
|
||||
|
||||
// RecyclerView
|
||||
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
|
||||
android:name=".app.Application"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@mipmap/ic_murena"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_murena_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
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.UNREGISTER"/>
|
||||
<action android:name="org.unifiedpush.android.distributor.feature.BYTES_MESSAGE"/>
|
||||
<action android:name="org.unifiedpush.android.distributor.feature.MURENA"/>
|
||||
</intent-filter>
|
||||
</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.app.Application
|
||||
import io.heckel.ntfy.db.Repository
|
||||
import io.heckel.ntfy.firebase.FirebaseMessenger
|
||||
import io.heckel.ntfy.msg.NotificationService
|
||||
import io.heckel.ntfy.util.Log
|
||||
import io.heckel.ntfy.util.topicUrl
|
||||
|
@ -18,7 +17,6 @@ class Backuper(val context: Context) {
|
|||
private val gson = Gson()
|
||||
private val resolver = context.applicationContext.contentResolver
|
||||
private val repository = (context.applicationContext as Application).repository
|
||||
private val messenger = FirebaseMessenger()
|
||||
private val notifier = NotificationService(context)
|
||||
|
||||
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)
|
||||
|
||||
// Subscribe to Firebase topics
|
||||
if (s.baseUrl == appBaseUrl) {
|
||||
messenger.subscribe(s.topic)
|
||||
}
|
||||
|
||||
// Create dedicated channels
|
||||
if (s.dedicatedChannels) {
|
||||
notifier.createSubscriptionNotificationChannels(subscription)
|
||||
|
|
|
@ -7,6 +7,7 @@ import io.heckel.ntfy.db.Subscription
|
|||
import io.heckel.ntfy.util.Log
|
||||
import io.heckel.ntfy.up.Distributor
|
||||
import io.heckel.ntfy.util.decodeBytesMessage
|
||||
import io.heckel.ntfy.util.decodeBytesTitle
|
||||
import io.heckel.ntfy.util.safeLet
|
||||
|
||||
/**
|
||||
|
@ -39,7 +40,12 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
|
|||
}
|
||||
if (distribute) {
|
||||
safeLet(subscription.upAppId, subscription.upConnectorToken) { appId, connectorToken ->
|
||||
distributor.sendMessage(appId, connectorToken, decodeBytesMessage(notification))
|
||||
distributor.sendMessage(
|
||||
appId,
|
||||
connectorToken,
|
||||
decodeBytesMessage(notification),
|
||||
decodeBytesTitle(notification),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (downloadAttachment && downloadIcon) {
|
||||
|
|
|
@ -90,11 +90,7 @@ class SubscriberService : Service() {
|
|||
Log.d(TAG, "Subscriber service has been created")
|
||||
|
||||
val title = getString(R.string.channel_subscriber_notification_title)
|
||||
val text = if (BuildConfig.FIREBASE_AVAILABLE) {
|
||||
getString(R.string.channel_subscriber_notification_instant_text)
|
||||
} else {
|
||||
getString(R.string.channel_subscriber_notification_noinstant_text)
|
||||
}
|
||||
val text = getString(R.string.channel_subscriber_notification_noinstant_text)
|
||||
notificationManager = createNotificationChannel()
|
||||
serviceNotification = createNotification(title, text)
|
||||
|
||||
|
@ -221,18 +217,7 @@ class SubscriberService : Service() {
|
|||
// Update foreground service notification popup
|
||||
if (connections.size > 0) {
|
||||
val title = getString(R.string.channel_subscriber_notification_title)
|
||||
val text = if (BuildConfig.FIREBASE_AVAILABLE) {
|
||||
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) {
|
||||
val text = when (instantSubscriptions.size) {
|
||||
1 -> getString(R.string.channel_subscriber_notification_noinstant_text_one)
|
||||
2 -> getString(R.string.channel_subscriber_notification_noinstant_text_two)
|
||||
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)
|
||||
else -> getString(R.string.channel_subscriber_notification_noinstant_text_more, instantSubscriptions.size)
|
||||
}
|
||||
}
|
||||
serviceNotification = createNotification(title, text)
|
||||
notificationManager?.notify(NOTIFICATION_SERVICE_ID, serviceNotification)
|
||||
}
|
||||
|
|
|
@ -114,10 +114,7 @@ class AddFragment : DialogFragment() {
|
|||
// Set foreground description text
|
||||
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
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
|
@ -291,16 +288,6 @@ class AddFragment : DialogFragment() {
|
|||
private fun validateInputSubscribeView() {
|
||||
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) {
|
||||
subscribeUseAnotherServerDescription.visibility = View.VISIBLE
|
||||
subscribeBaseUrlLayout.visibility = View.VISIBLE
|
||||
|
@ -308,15 +295,9 @@ class AddFragment : DialogFragment() {
|
|||
subscribeUseAnotherServerDescription.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
|
||||
subscribeInstantDeliveryDescription.visibility = View.GONE
|
||||
subscribeForegroundDescription.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
|
||||
}
|
||||
subscribeInstantDeliveryBox.visibility = View.GONE
|
||||
subscribeInstantDeliveryDescription.visibility = View.GONE
|
||||
subscribeForegroundDescription.visibility = View.GONE
|
||||
|
||||
// Enable/disable "Subscribe" button
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
|
@ -356,7 +337,7 @@ class AddFragment : DialogFragment() {
|
|||
activity.runOnUiThread {
|
||||
val topic = subscribeTopicText.text.toString()
|
||||
val baseUrl = getBaseUrl()
|
||||
val instant = !BuildConfig.FIREBASE_AVAILABLE || baseUrl != appBaseUrl || subscribeInstantDeliveryCheckbox.isChecked
|
||||
val instant = baseUrl != appBaseUrl || subscribeInstantDeliveryCheckbox.isChecked
|
||||
subscribeListener.onSubscribe(topic, baseUrl, instant)
|
||||
dialog?.dismiss()
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@ import io.heckel.ntfy.app.Application
|
|||
import io.heckel.ntfy.db.Notification
|
||||
import io.heckel.ntfy.db.Repository
|
||||
import io.heckel.ntfy.db.Subscription
|
||||
import io.heckel.ntfy.firebase.FirebaseMessenger
|
||||
import io.heckel.ntfy.util.Log
|
||||
import io.heckel.ntfy.msg.ApiService
|
||||
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 api = ApiService()
|
||||
private val messenger = FirebaseMessenger()
|
||||
private var notifier: NotificationService? = null // Context-dependent
|
||||
private var appBaseUrl: String? = null // Context-dependent
|
||||
|
||||
|
@ -78,8 +76,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
notifier = NotificationService(this)
|
||||
appBaseUrl = getString(R.string.app_base_url)
|
||||
|
||||
// Show 'Back' button
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||
|
||||
// Handle direct deep links to topic "ntfy://..."
|
||||
val url = intent?.data
|
||||
|
@ -90,6 +87,10 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
finishAffinity()
|
||||
}
|
||||
|
||||
private fun maybeSubscribeAndLoadView(url: Uri) {
|
||||
if (url.pathSegments.size != 1) {
|
||||
Log.w(TAG, "Invalid link $url. Aborting.")
|
||||
|
@ -127,12 +128,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
)
|
||||
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
|
||||
try {
|
||||
val user = repository.getUser(subscription.baseUrl) // May be null
|
||||
|
@ -176,24 +171,12 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
val topicUrl = topicShortUrl(subscriptionBaseUrl, subscriptionTopic)
|
||||
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
|
||||
mainListContainer = findViewById(R.id.detail_notification_list_container)
|
||||
mainListContainer.setOnRefreshListener { refresh() }
|
||||
mainListContainer.setColorSchemeResources(Colors.refreshProgressIndicator)
|
||||
|
||||
// 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 onNotificationLongClick = { n: Notification -> onNotificationLongClick(n) }
|
||||
|
||||
|
@ -207,10 +190,8 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
adapter.submitList(it as MutableList<Notification>)
|
||||
if (it.isEmpty()) {
|
||||
mainListContainer.visibility = View.GONE
|
||||
noEntriesText.visibility = View.VISIBLE
|
||||
} else {
|
||||
mainListContainer.visibility = View.VISIBLE
|
||||
noEntriesText.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Cancel notifications that still have popups
|
||||
|
@ -345,10 +326,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.detail_menu_test -> {
|
||||
onTestClick()
|
||||
true
|
||||
}
|
||||
R.id.detail_menu_notifications_enabled -> {
|
||||
onMutedUntilClick(enable = false)
|
||||
true
|
||||
|
@ -369,22 +346,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
onInstantEnableClick(enable = false)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -529,14 +490,8 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
val appBaseUrl = getString(R.string.app_base_url)
|
||||
val enableInstantItem = menu.findItem(R.id.detail_menu_enable_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
|
||||
disableInstantItem?.isVisible = false
|
||||
}
|
||||
enableInstantItem?.isVisible = false
|
||||
disableInstantItem?.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -608,9 +563,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
|||
GlobalScope.launch(Dispatchers.IO) {
|
||||
repository.removeAllNotifications(subscriptionId)
|
||||
repository.removeSubscription(subscriptionId)
|
||||
if (subscriptionBaseUrl == appBaseUrl) {
|
||||
messenger.unsubscribe(subscriptionTopic)
|
||||
}
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
|
|
@ -114,7 +114,6 @@ class DetailSettingsActivity : AppCompatActivity() {
|
|||
|
||||
private fun loadView() {
|
||||
if (subscription.upAppId == null) {
|
||||
loadInstantPref()
|
||||
loadMutedUntilPref()
|
||||
loadMinPriorityPref()
|
||||
loadAutoDeletePref()
|
||||
|
@ -134,29 +133,6 @@ class DetailSettingsActivity : AppCompatActivity() {
|
|||
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() {
|
||||
val prefId = context?.getString(R.string.detail_settings_notifications_dedicated_channels_key) ?: return
|
||||
val pref: SwitchPreference? = findPreference(prefId)
|
||||
|
|
|
@ -34,13 +34,13 @@ import io.heckel.ntfy.R
|
|||
import io.heckel.ntfy.app.Application
|
||||
import io.heckel.ntfy.db.Repository
|
||||
import io.heckel.ntfy.db.Subscription
|
||||
import io.heckel.ntfy.firebase.FirebaseMessenger
|
||||
import io.heckel.ntfy.msg.ApiService
|
||||
import io.heckel.ntfy.msg.DownloadManager
|
||||
import io.heckel.ntfy.msg.DownloadType
|
||||
import io.heckel.ntfy.msg.NotificationDispatcher
|
||||
import io.heckel.ntfy.service.SubscriberService
|
||||
import io.heckel.ntfy.service.SubscriberServiceManager
|
||||
import io.heckel.ntfy.up.TOPIC_MURENA
|
||||
import io.heckel.ntfy.util.*
|
||||
import io.heckel.ntfy.work.DeleteWorker
|
||||
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 api = ApiService()
|
||||
private val messenger = FirebaseMessenger()
|
||||
|
||||
// UI elements
|
||||
private lateinit var menu: Menu
|
||||
private lateinit var mainList: RecyclerView
|
||||
private lateinit var mainListContainer: SwipeRefreshLayout
|
||||
private lateinit var adapter: MainAdapter
|
||||
private lateinit var fab: FloatingActionButton
|
||||
|
||||
// Other stuff
|
||||
private var actionMode: ActionMode? = null
|
||||
|
@ -87,12 +85,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
// Action bar
|
||||
title = getString(R.string.main_action_bar_title)
|
||||
|
||||
// Floating action button ("+")
|
||||
fab = findViewById(R.id.fab)
|
||||
fab.setOnClickListener {
|
||||
onSubscribeButtonClick()
|
||||
}
|
||||
|
||||
// Swipe to refresh
|
||||
mainListContainer = findViewById(R.id.main_subscriptions_list_container)
|
||||
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
|
||||
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
|
||||
AppCompatDelegate.setDefaultNightMode(repository.getDarkMode())
|
||||
|
||||
|
@ -212,6 +201,42 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
|
||||
// Permissions
|
||||
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() {
|
||||
|
@ -349,10 +374,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
}
|
||||
val mutedUntilSeconds = repository.getGlobalMutedUntil()
|
||||
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
|
||||
val notificationsEnabledItem = menu.findItem(R.id.main_menu_notifications_enabled)
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -466,12 +463,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
)
|
||||
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
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
|
@ -635,18 +626,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
actionMode = startActionMode(this)
|
||||
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
|
||||
val fromColor = ContextCompat.getColor(this, Colors.statusBarNormal(this))
|
||||
val toColor = ContextCompat.getColor(this, Colors.statusBarActionMode(this))
|
||||
|
@ -663,19 +642,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
adapter.selected.clear()
|
||||
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
|
||||
val fromColor = ContextCompat.getColor(this, Colors.statusBarActionMode(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
|
||||
notificationDisabledUntilImageView.visibility = if (showMutedUntilIcon) 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) {
|
||||
newItemsView.visibility = View.GONE
|
||||
} else {
|
||||
|
|
|
@ -68,15 +68,23 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
|
|||
|
||||
// Add subscription
|
||||
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 subscriptionId =
|
||||
appId.takeIf { it == PACKAGE_MURENA_UNIFIED_PUSH }?.run {
|
||||
repository.getSubscriptions()
|
||||
.find { it.baseUrl == baseUrl && it.topic == topic }?.id
|
||||
} ?: randomSubscriptionId()
|
||||
val subscription = Subscription(
|
||||
id = randomSubscriptionId(),
|
||||
id = subscriptionId,
|
||||
baseUrl = baseUrl,
|
||||
topic = topic,
|
||||
instant = true, // No Firebase, always instant!
|
||||
dedicatedChannels = false,
|
||||
mutedUntil = 0,
|
||||
mutedUntil = Repository.MUTED_UNTIL_FOREVER,
|
||||
minPriority = Repository.MIN_PRIORITY_USE_GLOBAL,
|
||||
autoDelete = Repository.AUTO_DELETE_USE_GLOBAL,
|
||||
insistent = Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL,
|
||||
|
@ -89,10 +97,15 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
|
|||
newCount = 0,
|
||||
lastActive = Date().time/1000
|
||||
)
|
||||
Log.d(TAG, "Adding subscription with for app $appId (connectorToken $connectorToken): $subscription")
|
||||
try {
|
||||
// Note, this may fail due to a SQL constraint exception, see https://github.com/binwiederhier/ntfy/issues/185
|
||||
repository.addSubscription(subscription)
|
||||
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)
|
||||
}
|
||||
distributor.sendEndpoint(appId, connectorToken, endpoint)
|
||||
|
||||
// Refresh (and maybe start) foreground service
|
||||
|
|
|
@ -19,4 +19,9 @@ const val EXTRA_APPLICATION = "application"
|
|||
const val EXTRA_TOKEN = "token"
|
||||
const val EXTRA_ENDPOINT = "endpoint"
|
||||
const val EXTRA_MESSAGE = "message"
|
||||
const val EXTRA_TITLE = "title"
|
||||
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.
|
||||
*/
|
||||
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")
|
||||
val broadcastIntent = Intent()
|
||||
broadcastIntent.`package` = app
|
||||
broadcastIntent.action = ACTION_MESSAGE
|
||||
broadcastIntent.putExtra(EXTRA_TOKEN, connectorToken)
|
||||
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_TITLE, title)
|
||||
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 {
|
||||
if (notification.encoding == MESSAGE_ENCODING_BASE64) {
|
||||
Base64.decode(notification.message, Base64.DEFAULT)
|
||||
Base64.decode(string, Base64.DEFAULT)
|
||||
} else {
|
||||
notification.message.toByteArray()
|
||||
string.toByteArray()
|
||||
}
|
||||
} 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.
|
||||
* 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"/>
|
||||
</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>
|
||||
|
|
|
@ -174,7 +174,8 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="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">
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
|
@ -208,16 +209,4 @@
|
|||
android:autoLink="web"/>
|
||||
</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>
|
||||
|
|
|
@ -9,9 +9,4 @@
|
|||
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"
|
||||
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>
|
||||
|
|
|
@ -5,9 +5,4 @@
|
|||
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"
|
||||
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>
|
||||
|
|
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
|
||||
-->
|
||||
|
||||
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<item name="colorPrimary">@color/teal_light</item>
|
||||
<item name="colorAccent">@color/teal_light</item> <!-- checkboxes, text fields -->
|
||||
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">@color/e_accent_dark</item>
|
||||
<item name="colorAccent">@color/e_accent_dark</item> <!-- checkboxes, text fields -->
|
||||
<item name="android:colorBackground">@color/black_900</item> <!-- background -->
|
||||
<item name="android:statusBarColor">@color/black_900</item>
|
||||
<item name="actionModeBackground">@color/black_900</item>
|
||||
<item name="android:statusBarColor">@color/e_accent_dark</item>
|
||||
<item name="actionModeBackground">@color/e_accent_dark</item>
|
||||
|
||||
<!-- Action bar background & text color -->
|
||||
<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="red_light">#fe4d2e</color> <!-- Danger text (dark 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>
|
||||
|
||||
|
|
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>
|
||||
<!-- Main app theme; dark theme styles see values-night/styles.xml -->
|
||||
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<item name="colorPrimary">@color/teal</item>
|
||||
<item name="colorAccent">@color/teal</item> <!-- checkboxes, text fields -->
|
||||
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">@color/e_accent_light</item>
|
||||
<item name="colorAccent">@color/e_accent_light</item> <!-- checkboxes, text fields -->
|
||||
<item name="android:colorBackground">@color/white</item> <!-- background -->
|
||||
<item name="android:statusBarColor">@color/teal</item>
|
||||
<item name="actionModeBackground">@color/teal_dark</item>
|
||||
<item name="android:statusBarColor">@color/e_accent_light</item>
|
||||
<item name="actionModeBackground">@color/e_accent_light</item>
|
||||
</style>
|
||||
|
||||
<style name="DangerText" parent="@android:style/TextAppearance">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
The translatable="false" attribute is just an additional safety. -->
|
||||
|
||||
<!-- 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! -->
|
||||
|
||||
<!-- Main activity -->
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
app:entryValues="@array/settings_general_dark_mode_values"
|
||||
app:defaultValue="-1"/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory app:title="@string/settings_backup_restore_header">
|
||||
<PreferenceCategory app:title="@string/settings_backup_restore_header" app:isPreferenceVisible="false">
|
||||
<ListPreference
|
||||
app:key="@string/settings_backup_restore_backup_key"
|
||||
app:title="@string/settings_backup_restore_backup_title"
|
||||
|
@ -96,7 +96,7 @@
|
|||
app:title="@string/settings_advanced_clear_logs_title"
|
||||
app:summary="@string/settings_advanced_clear_logs_summary"/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory app:title="@string/settings_about_header">
|
||||
<PreferenceCategory app:title="@string/settings_about_header" app:isPreferenceVisible="false">
|
||||
<Preference
|
||||
app:key="@string/settings_about_version_key"
|
||||
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"
|
||||
}
|
||||
}
|