Android Question Get push notification from opensource ntfy.sh

GJREDITOR

Member
To get push notifications from ntfy (ntfy.sh), my app has to register to intents with the "io.heckel.ntfy.MESSAGE_RECEIVED" action. I have installed the ntfy app . In my own app I have the following code in manifest:
Manifest:
AddManifestText(
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33"/>
- -- ------
----------
'End of default text.
AddReceiverText(ntfyPush,
<intent-filter>
    <action android:name="io.heckel.ntfy.MESSAGE_RECEIVED" />
</intent-filter>)
In Receiver Module named ntfyPush, I have this code:
ntfyPush Receiver Module:
Private Sub Receiver_Receive (FirstTime As Boolean, StartingIntent As Intent)
    Log(StartingIntent)
    Dim b As Beeper
    b.Initialize(500,500)
    b.Beep
    Sleep(500)
    b.Release
    If StartingIntent.Action = "io.heckel.ntfy.MESSAGE_RECEIVED" Then
        Log("Topic: " & StartingIntent.GetExtra("topic"))
        ToastMessageShow(StartingIntent.GetExtra("topic"),True)
        Log("Message: " & StartingIntent.GetExtra("message"))
        ToastMessageShow(StartingIntent.GetExtra("message"),True)
    End If
End Sub
However I am not getting any notification when I try to send notification using ntfy web app . Do I need to do anything else apart from what I have done above to get push notification?
 

GJREDITOR

Member
Thank you . Please find below relevant code:
React to incoming messages:
To react on incoming notifications, you have to register to intents with the io.heckel.ntfy.MESSAGE_RECEIVED action. Any app that can catch broadcasts is supported:
The relevant Github code is here: and reproduced below:
ntfy app receiving broadcast intents:
/**
 * The broadcast service is responsible for sending and receiving broadcast intents
 * in order to facilitate tasks app integrations.
 */
class BroadcastService(private val ctx: Context) {
    fun sendMessage(subscription: Subscription, notification: Notification, muted: Boolean) {
        val intent = Intent()
        intent.action = MESSAGE_RECEIVED_ACTION
        intent.putExtra("id", notification.id)
        intent.putExtra("base_url", subscription.baseUrl)
        intent.putExtra("topic", subscription.topic)
        intent.putExtra("time", notification.timestamp.toInt())
        intent.putExtra("title", notification.title)
        intent.putExtra("message", decodeMessage(notification))
        intent.putExtra("message_bytes", decodeBytesMessage(notification))
        intent.putExtra("message_encoding", notification.encoding)
        intent.putExtra("tags", notification.tags)
        intent.putExtra("tags_map", joinTagsMap(splitTags(notification.tags)))
        intent.putExtra("priority", notification.priority)
        intent.putExtra("click", notification.click)
        intent.putExtra("muted", muted)
        intent.putExtra("muted_str", muted.toString())
        intent.putExtra("attachment_name", notification.attachment?.name ?: "")
        intent.putExtra("attachment_type", notification.attachment?.type ?: "")
        intent.putExtra("attachment_size", notification.attachment?.size ?: 0L)
        intent.putExtra("attachment_expires", notification.attachment?.expires ?: 0L)
        intent.putExtra("attachment_url", notification.attachment?.url ?: "")

        Log.d(TAG, "Sending message intent broadcast: ${intent.action} with extras ${intent.extras}")
        ctx.sendBroadcast(intent)
    }

    fun sendUserAction(action: Action) {
        val intent = Intent()
        intent.action = action.intent ?: USER_ACTION_ACTION
        action.extras?.forEach { (key, value) ->
            intent.putExtra(key, value)
        }
        Log.d(TAG, "Sending user action intent broadcast: ${intent.action} with extras ${intent.extras}")
        ctx.sendBroadcast(intent)
    }

    /**
     * This receiver is triggered when the SEND_MESSAGE intent is received.
     * See AndroidManifest.xml for details.
     */
    class BroadcastReceiver : android.content.BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            Log.d(TAG, "Broadcast received: $intent")
            when (intent.action) {
                MESSAGE_SEND_ACTION -> send(context, intent)
            }
        }

        private fun send(ctx: Context, intent: Intent) {
            val api = ApiService()
            val baseUrl = getStringExtra(intent, "base_url") ?: ctx.getString(R.string.app_base_url)
            val topic = getStringExtra(intent, "topic") ?: return
            val message = getStringExtra(intent, "message") ?: return
            val title = getStringExtra(intent, "title") ?: ""
            val tags = getStringExtra(intent,"tags") ?: ""
            val priority = when (getStringExtra(intent, "priority")) {
                "min", "1" -> 1
                "low", "2" -> 2
                "default", "3" -> 3
                "high", "4" -> 4
                "urgent", "max", "5" -> 5
                else -> 0
            }
            val delay = getStringExtra(intent,"delay") ?: ""
            GlobalScope.launch(Dispatchers.IO) {
                val repository = Repository.getInstance(ctx)
                val user = repository.getUser(baseUrl) // May be null
                try {
                    Log.d(TAG, "Publishing message $intent")
                    api.publish(
                        baseUrl = baseUrl,
                        topic = topic,
                        user = user,
                        message = message,
                        title = title,
                        priority = priority,
                        tags = splitTags(tags),
                        delay = delay
                    )
                } catch (e: Exception) {
                    Log.w(TAG, "Unable to publish message: ${e.message}", e)
                }
            }
        }

        /**
         * Gets an extra as a String value, even if the extra may be an int or a long.
         */
        private fun getStringExtra(intent: Intent, name: String): String? {
            if (intent.getStringExtra(name) != null) {
                return intent.getStringExtra(name)
            } else if (intent.getIntExtra(name, DOES_NOT_EXIST) != DOES_NOT_EXIST) {
                return intent.getIntExtra(name, DOES_NOT_EXIST).toString()
            } else if (intent.getLongExtra(name, DOES_NOT_EXIST.toLong()) != DOES_NOT_EXIST.toLong()) {
                return intent.getLongExtra(name, DOES_NOT_EXIST.toLong()).toString()
            }
            return null
        }
    }

    companion object {
        private const val TAG = "NtfyBroadcastService"
        private const val DOES_NOT_EXIST = -2586000

        // These constants cannot be changed without breaking the contract; also see manifest
        private const val MESSAGE_RECEIVED_ACTION = "io.heckel.ntfy.MESSAGE_RECEIVED"
        private const val MESSAGE_SEND_ACTION = "io.heckel.ntfy.SEND_MESSAGE"
        private const val USER_ACTION_ACTION = "io.heckel.ntfy.USER_ACTION"
    }
}

The Android Manifest code is here and reproduced below:
Android Manifest Code:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="io.heckel.ntfy">

    <!-- Permissions -->
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <!-- For instant delivery foregrounds service -->
    <uses-permission android:name="android.permission.WAKE_LOCK"/> <!-- To keep foreground service awake; soon not needed anymore -->
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <!-- To restart service on reboot -->
    <uses-permission android:name="android.permission.VIBRATE"/> <!-- Incoming notifications should be able to vibrate the phone -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/> <!-- Only required on SDK <= 28 -->
    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <!-- To reschedule the websocket retry -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- As of Android 13, we need to ask for permission to post notifications -->

    <!--
        Permission REQUEST_INSTALL_PACKAGES (F-Droid only!):
          - Permission is used to install .apk files that were received as attachments
          - Google rejected the permission for ntfy, so this permission is STRIPPED OUT by the build process
            for the Google Play variant of the app.
    -->
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

    <application
            android:name=".app.Application"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher"
            android:supportsRtl="true"
            android:theme="@style/AppTheme"
            android:networkSecurityConfig="@xml/network_security_config"
            android:usesCleartextTraffic="true">

        <!-- Main activity -->
        <activity
                android:name=".ui.MainActivity"
                android:label="@string/app_name"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <!-- Detail activity -->
        <activity
                android:name=".ui.DetailActivity"
                android:parentActivityName=".ui.MainActivity"
                android:exported="true">
            <meta-data
                    android:name="android.support.PARENT_ACTIVITY"
                    android:value=".ui.MainActivity"/>

            <!-- Open ntfy:// links with the app -->
            <intent-filter android:label="@string/app_name">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="ntfy" />
            </intent-filter>
        </activity>

        <!-- Settings activity -->
        <activity
                android:name=".ui.SettingsActivity"
                android:parentActivityName=".ui.MainActivity">
            <meta-data
                    android:name="android.support.PARENT_ACTIVITY"
                    android:value=".ui.MainActivity"/>
        </activity>

        <!-- Detail settings activity -->
        <activity
                android:name=".ui.DetailSettingsActivity"
                android:parentActivityName=".ui.DetailActivity">
        </activity>

        <!-- Share file activity, incoming files/shares -->
        <activity android:name=".ui.ShareActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.SEND" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="image/*" />
                <data android:mimeType="text/*" />
                <data android:mimeType="audio/*" />
                <data android:mimeType="video/*" />
                <data android:mimeType="application/*" />
            </intent-filter>
        </activity>

        <!-- Hack: Activity used for "view" action button with "clear=true" (to be able to cancel notifications and show a URL) -->
        <activity
                android:name=".msg.NotificationService$ViewActionWithClearActivity"
                android:exported="false">
        </activity>

        <!-- Subscriber foreground service for hosts other than ntfy.sh -->
        <service android:name=".service.SubscriberService"/>

        <!-- Subscriber service restart on reboot -->
        <receiver
                android:name=".service.SubscriberService$BootStartReceiver"
                android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>

        <!-- Subscriber service restart on destruction -->
        <receiver
                android:name=".service.SubscriberService$AutoRestartReceiver"
                android:enabled="true"
                android:exported="false"/>

        <!-- Broadcast receiver to send messages via intents -->
        <receiver
                android:name=".msg.BroadcastService$BroadcastReceiver"
                android:enabled="true"
                android:exported="true">
            <intent-filter>
                <action android:name="io.heckel.ntfy.SEND_MESSAGE"/>
            </intent-filter>
        </receiver>

        <!-- Broadcast receiver for UnifiedPush; must match https://github.com/UnifiedPush/UP-spec/blob/main/specifications.md -->
        <receiver
                android:name=".up.BroadcastReceiver"
                android:enabled="true"
                android:exported="true">
            <intent-filter>
                <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"/>
            </intent-filter>
        </receiver>

        <!-- Broadcast receiver for the "Download"/"Cancel" attachment action in the notification popup -->
        <receiver
                android:name=".msg.NotificationService$UserActionBroadcastReceiver"
                android:enabled="true"
                android:exported="false">
        </receiver>

        <!-- Broadcast receiver for when the notification is swiped away (currently only to cancel the insistent sound) -->
        <receiver
                android:name=".msg.NotificationService$DeleteBroadcastReceiver"
                android:enabled="true"
                android:exported="false">
        </receiver>

        <!-- Firebase messaging (note that this is empty in the F-Droid flavor) -->
        <service
                android:name=".firebase.FirebaseService"
                android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT"/>
            </intent-filter>
        </service>
        <meta-data
                android:name="firebase_analytics_collection_enabled"
                android:value="false"/>
        <meta-data
                android:name="com.google.firebase.messaging.default_notification_icon"
                android:resource="@drawable/ic_notification"/>

        <!-- FileProvider required for older Android versions (<= P), to allow passing the file URI in the open intent.
             Avoids "exposed beyond app through Intent.getData" exception, see see https://stackoverflow.com/a/57288352/1440785 -->
        <provider
                android:name="androidx.core.content.FileProvider"
                android:authorities="${applicationId}.provider"
                android:exported="false"
                android:grantUriPermissions="true">
            <meta-data
                    android:name="android.support.FILE_PROVIDER_PATHS"
                    android:resource="@xml/file_paths"/>
        </provider>
    </application>
</manifest>
 
Upvote 0

DonManfred

Expert
Licensed User
Longtime User
No need for any broadcast listener, no need for any 3rdparty app.

Integrate Firebase in ntfy.sh and use the Firebasemessaging in B4A.


FCM is the only method that an Android app can receive messages without having to run a foreground service.
 
Upvote 1
Solution

GJREDITOR

Member
Thanks. I now realize that no other push notification apart from FCM can run without foreground App.
 
Upvote 0
Cookies are required to use this site. You must accept them to continue using the site. Learn more…