Search code examples
javaandroidkotlinbroadcastreceiverandroid-broadcast

onReceive doesn't work on BroadcastReceiver


I need to know what the user selected in order to share content for analytics. For this, I use the Google ShareSheet.

To obtain the value selected by the user from the Google ShareSheet, I use a PendingIntent. Here's how I've implemented it:

...
override fun doOpenShareSheet(
        activity: AppCompatActivity
    ) {
        var flag: Int = PendingIntent.FLAG_UPDATE_CURRENT

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            flag = PendingIntent.FLAG_IMMUTABLE
        }

        val pi: PendingIntent = PendingIntent.getBroadcast(
            activity,
            0,
            Intent(activity, ChooserBroadcastReceiver::class.java),
            flag
        )

        // Implement Google ShareSheet
        val share: Intent = Intent.createChooser(
            Intent().apply {
                action = Intent.ACTION_SEND
                putExtra(Intent.EXTRA_TEXT, config.url)

                // Passing content URI to image to be displayed
                flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
                type = "application/url"
            },
            config.mediaTitle,
            pi.intentSender
        )

        activity.startActivity(share)
    }
...

Now, to get the value, I need to register the BroadcastReceiver dynamically (as I cannot pass the value to the receiver's constructor in the Manifest).

Here's what I have:

...
class ChooserBroadcastReceiver(private val vm: MyViewModel): BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        val selectedComponent: String = intent?.extras?.get(Intent.EXTRA_CHOSEN_COMPONENT).toString()
        vm.doOnChooserBroadcastReceiver(selectedComponent = selectedComponent)
    }
}
....

@AndroidEntryPoint
class MyFragment : Fragment() {
...
    override fun onResume() {
        super.onResume()
        registerBroadcast()
    }

    private fun registerBroadcast() {
        chooserBroadcastReceiver = ChooserBroadcastReceiver(vm = vm)
        val intentFilter = IntentFilter()
        intentFilter.addAction(Intent.ACTION_SEND)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            requireActivity().registerReceiver(chooserBroadcastReceiver, intentFilter, RECEIVER_EXPORTED)
        } else {
            requireActivity().registerReceiver(chooserBroadcastReceiver, intentFilter)
        }
    }
...
}

However, for some reason, I'm not receiving the value. I can see that the event is happening, and I can choose how to share the content through the Google ShareSheet, but the event is not received in onReceive.

P.S. For the test, I tried removing the view model from the constructor and registered the receiver through the manifest, and everything worked. Here's how:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application>
        <receiver
            android:name=".ui.MyBroadcastReceiver"
            android:exported="false">
        </receiver>
    </application>
</manifest>

But I need to pass a parameter to the constructor, so this method doesn't work for me.

What am I doing wrong? Why does it work when registered through the manifest, but the event is not received when registered dynamically in the code?


Solution

  • I cannot pass the value to the receiver's constructor in the Manifest

    Putting a ViewModel in the receiver's constructor is kinda weird. Regardless, your PendingIntent and your IntentFilter do not match.

            val pi: PendingIntent = PendingIntent.getBroadcast(
                activity,
                0,
                Intent(activity, ChooserBroadcastReceiver::class.java),
                flag
            )
    

    This says "look in the manifest for ChooserBroadcastReceiver and send the broadcast to it".

            val intentFilter = IntentFilter()
            intentFilter.addAction(Intent.ACTION_SEND)
    

    This says "respond to ACTION_SEND broadcasts" (which itself is weird, as ACTION_SEND is usually an activity action).

    You need those things to match. If you do not wish to register your receiver in the manifest, then you need to use a PendingIntent that creates an Intent that matches your IntentFilter.

            val pi: PendingIntent = PendingIntent.getBroadcast(
                activity,
                0,
                Intent(Intent.ACTION_SEND),
                flag
            )
    

    Also, please note that on most versions of Android, any app on the system can send a broadcast to that receiver, as it will be exported.

    But I need to pass a parameter to the constructor,

    You could use dependency inversion (Dagger/Hilt, Koin, etc.). That may be difficult to get a suitable Jetpack ViewModel, but you could inject a repository or use case or something.