Filtering Foreground Notifications

Your notifications work great, but they are sent even when the user already has the application open. You can use broadcast intents to change the behavior of PollWorker based on whether your app is in the foreground.

First, you will send a broadcast intent from your PollWorker whenever new photos are fetched. Next, you will register two broadcast receivers. The first receiver will be registered in your Android manifest. Whenever it receives a broadcast from PollWorker, it will post the notification to the user as you had done before. The second receiver will be registered dynamically so that it is only active when your application is visible to the user. Its job will be to intercept broadcasts from being delivered to the broadcast receiver in your manifest, which will prevent it from posting a notification.

It may sound unusual to use two broadcast receivers to accomplish this, but Android does not provide a mechanism to determine which activities or fragments are currently running. Since PollWorker cannot directly tell whether your UI is visible, you would not be able skip posting the notification using a simple if statement in PollWorker. Likewise, you cannot choose to conditionally send the broadcast based on whether PhotoGallery is visible, since there is not a way to determine which state your application is in. You can, however, use two receivers and set them up so that only one will react to the broadcast. That is what you will be doing.

Sending broadcast intents

The first step is straightforward: You need to send your own broadcast intent notifying interested components that a new search results notification is ready to post. To send a broadcast intent, create an intent and pass it into sendBroadcast(Intent). In this case, you will want it to broadcast an action you define, so define an action constant as well.

Update PollWorker.kt as shown.

Listing 28.1  Sending a broadcast intent (PollWorker.kt)

class PollWorker(val context: Context, workerParams: WorkerParameters) :
    Worker(context, workerParams) {

    override fun doWork(): Result {
        ...
        val resultId = first().id
        if (resultId == lastResultId) {
            Log.i(TAG, "Got an old result: $resultId")
        } else {
            ...
            val notificationManager = NotificationManagerCompat.from(context)
            notificationManager.notify(0, notification)

            context.sendBroadcast(Intent(ACTION_SHOW_NOTIFICATION))
        }

        return Result.success()
    }

    companion object {
        const val ACTION_SHOW_NOTIFICATION =
            "com.bignerdranch.android.photogallery.SHOW_NOTIFICATION"
    }
}

Creating and registering a standalone receiver

Although your broadcast is being sent, nobody is listening for it yet. To react to broadcasts, you will implement a BroadcastReceiver. There are two kinds of broadcast receivers; here you will be using a standalone broadcast receiver.

A standalone broadcast receiver is a receiver that is declared in the manifest. Such a receiver can be activated even if your app process is dead. Later you will learn about dynamic broadcast receivers, which can instead be tied to the lifecycle of a visible app component, like a fragment or activity.

Just like services and activities, broadcast receivers must be registered with the system to do anything useful. If the receiver is not registered with the system, the system will never call its onReceive(…) function.

Before you can register your broadcast receiver, you have to write it. Create a new Kotlin class called NotificationReceiver that is a subclass of android.content.BroadcastReceiver.

Listing 28.2  Your first broadcast receiver (NotificationReceiver.kt)

private const val TAG = "NotificationReceiver"

class NotificationReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        Log.i(TAG, "received broadcast: ${intent.action}")
    }
}

A broadcast receiver is a component that receives intents, just like a service or an activity. When an intent is issued to NotificationReceiver, its onReceive(…) function will be called.

Next, open manifests/AndroidManifest.xml and hook up NotificationReceiver as a standalone receiver.

Listing 28.3  Adding your receiver to the manifest (manifests/AndroidManifest.xml)

<application ... >
    <activity android:name=".PhotoGalleryActivity">
        ...
    </activity>
    <receiver android:name=".NotificationReceiver">
    </receiver>
</application>

To receive the broadcasts that your receiver is interested in, your receiver should also have an intent filter. This filter will behave exactly like the ones you used with implicit intents except that it will filter broadcast intents instead of regular intents. Add an intent filter to your broadcast receiver to accept intents with the SHOW_NOTIFICATION action.

Listing 28.4  Adding an intent filter to your receiver (manifests/AndroidManifest.xml)

<receiver android:name=".NotificationReceiver">
  <intent-filter>
      <action
          android:name="com.bignerdranch.android.photogallery.SHOW_NOTIFICATION" />
  </intent-filter>
</receiver>

If you run PhotoGallery on a device running Android Oreo or higher after making these changes, you will not see your log. In fact, your receiver’s onReceive(…) function will not be called at all. But on older versions of Android, your log will appear as you would expect. This is because of restrictions that newer versions of Android place on broadcasts. But have no fear – your work thus far is not in vain.

You can get around these restrictions by sending your broadcast with a permission.

Limiting broadcasts to your app using private permissions

One issue with a broadcast like this is that anyone on the system can listen to it or trigger your receivers. You are usually not going to want either of those things to happen. Conveniently, specifying a permission on your broadcast will also make your new broadcast receiver work on newer versions of Android, so you can fix two bugs for the price of one.

You can preclude these unauthorized intrusions into your personal business by applying a custom permission to the receiver and setting the receiver’s android:exported attribute to "false". This prevents the receiver from being visible to other applications on the system. Applying a permission means only components that have requested (and been granted) permission can deliver a broadcast to the receiver.

First, declare and acquire your own permission in AndroidManifest.xml.

Listing 28.5  Adding a private permission (manifests/AndroidManifest.xml)

<manifest ... >

    <permission android:name="com.bignerdranch.android.photogallery.PRIVATE"
                android:protectionLevel="signature" />

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="com.bignerdranch.android.photogallery.PRIVATE" />
      ...
</manifest>

Notice that you define a custom permission with a protection level of signature. You will learn more about protection levels in just a moment. The permission itself is a simple string, just like the intent actions, categories, and system permissions you have used. You must always acquire a permission to use it, even when you defined it yourself. Them’s the rules.

Take note of the shaded constant value above, by the way. This string is the unique identifier for the custom permission, which you will use to refer to the permission elsewhere in your manifest and also from your Kotlin code when you issue a broadcast intent to the receiver. The identifier must be identical in each place. You would be wise to copy and paste it rather than typing it out by hand.

Next, apply the permission to the receiver tag and set the exported attribute to "false".

Listing 28.6  Applying permission and setting exported to "false" (manifests/AndroidManifest.xml)

<manifest ... >
    ...
    <application ... >
        ...
        <receiver android:name=".NotificationReceiver"
                  android:permission="com.bignerdranch.android.photogallery.PRIVATE"
                  android:exported="false">
                  ...
        </receiver>
    </application>
</manifest>

Now, use your permission by defining a corresponding constant in code and then passing it into your sendBroadcast(…) call.

Listing 28.7  Sending with a permission (PollWorker.kt)

class PollWorker(val context: Context, workerParams: WorkerParameters) :
    Worker(context, workerParams) {

    override fun doWork(): Result {
        ...
        val resultId = first().id
        if (resultId == lastResultId) {
            Log.i(TAG, "Got an old result: $resultId")
        } else {
            ...
            val notificationManager = NotificationManagerCompat.from(context)
            notificationManager.notify(0, notification)

            context.sendBroadcast(Intent(ACTION_SHOW_NOTIFICATION), PERM_PRIVATE)
        }

        return Result.success()
    }

    companion object {
        const val ACTION_SHOW_NOTIFICATION =
            "com.bignerdranch.android.photogallery.SHOW_NOTIFICATION"
        const val PERM_PRIVATE = "com.bignerdranch.android.photogallery.PRIVATE"
    }
}

Now, your app is the only app that can trigger that receiver. Run PhotoGallery again. You should see your log from NotificationReceiver appear in Logcat (though your notifications do not yet know the proper etiquette and will continue to display while the app is in the foreground – just as they did before this chapter).

More about protection levels

Every custom permission has to specify a value for android:protectionLevel. Your permission’s protectionLevel tells Android how it should be used. In your case, you used a protectionLevel of signature.

The signature protection level means that if another application wants to use your permission, it has to be signed with the same key as your application. This is usually the right choice for permissions you use internally in your application. Because other developers do not have your key, they cannot get access to anything this permission protects. Plus, because you do have your own key, you can use this permission in any other app you decide to write later.

The four available protection levels are summarized in Table 28.1:

Table 28.1  Values for protectionLevel

Value Description
normal This is for protecting app functionality that will not do anything dangerous, like accessing secure personal data or finding out where you are on a map. The user can see the permission before choosing to install the app but is never explicitly asked to grant it. android.permission.INTERNET uses this permission level, and so does the permission that lets your app vibrate the user’s device.
dangerous This is for most of the things you would not use normal for – accessing personal data, accessing hardware that might be used to spy on the user, or anything else that could cause real problems. The camera permission, locations permission, and contacts permission all fall under this category. Starting in Marshmallow, dangerous permissions require that you call requestPermission(…) at runtime to ask the user to explicitly grant your app permission.
signature The system grants this permission if the app is signed with the same certificate as the declaring application and denies it otherwise. If the permission is granted, the user is not notified. This is for functionality that is internal to an app – as the developer, because you have the certificate and only apps signed with the same certificate can use the permission, you have control over who uses the permission. You used it here to prevent anyone else from seeing your broadcasts. If you wanted, you could write another app that listens to them, too.
signatureOrSystem This is like signature, but it also grants permission to all packages in the Android system image. This is for communicating with apps built into the system image. If the permission is granted, the user is not notified. Most developers will not need to use this permission level, because it is intended to be used by hardware vendors.

Creating and registering a dynamic receiver

Next, you need a receiver for your ACTION_SHOW_NOTIFICATION broadcast intent. The job of this receiver will be to prevent posting a notification while the user is using your app.

This receiver is only going to be registered while your activity is in the foreground. If this receiver were declared with a longer lifespan (such as the lifetime of your app’s process), then you would need some other way of knowing that PhotoGalleryFragment is running (which would defeat the purpose of having this dynamic receiver in the first place).

The solution is to use a dynamic broadcast receiver. You register the receiver by calling Context.registerReceiver(BroadcastReceiver, IntentFilter) and unregister it by calling Context.unregisterReceiver(BroadcastReceiver). The receiver itself is typically defined as an inner class or a lambda, like a button-click listener. However, since you need the same instance in registerReceiver(…) and unregisterReceiver(…), you will need to assign the receiver to an instance variable.

Create a new abstract class called VisibleFragment with Fragment as its superclass. This class will be a generic fragment that hides foreground notifications. (You will write another fragment like this in Chapter 29.)

Listing 28.8  A receiver of VisibleFragment’s own (VisibleFragment.kt)

abstract class VisibleFragment : Fragment() {

    private val onShowNotification = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            Toast.makeText(requireContext(),
                    "Got a broadcast: ${intent.action}",
                    Toast.LENGTH_LONG)
                    .show()
        }
    }

    override fun onStart() {
        super.onStart()
        val filter = IntentFilter(PollWorker.ACTION_SHOW_NOTIFICATION)
        requireActivity().registerReceiver(
            onShowNotification,
            filter,
            PollWorker.PERM_PRIVATE,
            null
        )
    }

    override fun onStop() {
        super.onStop()
        requireActivity().unregisterReceiver(onShowNotification)
    }
}

Note that to pass in an IntentFilter, you have to create one in code. Your filter here is identical to the filter specified by the following XML:

    <intent-filter>
        <action android:name=
                "com.bignerdranch.android.photogallery.SHOW_NOTIFICATION" />
    </intent-filter>

Any IntentFilter you can express in XML can also be expressed in code this way. Just call addCategory(String), addAction(String), addDataPath(String), and so on to configure your filter.

When you use dynamically registered broadcast receivers, you must also take care to clean them up. Typically, if you register a receiver in a startup lifecycle function, you call Context.unregisterReceiver(BroadcastReceiver) in the corresponding shutdown function. Here, you register inside onStart() and unregister inside onStop(). If instead you registered inside onCreate(…), you would unregister inside onDestroy().

(Be careful with onCreate(…) and onDestroy() in retained fragments, by the way. getActivity() will return different values in onCreate(…) and onDestroy() if the screen has rotated. If you want to register and unregister in Fragment.onCreate(…) and Fragment.onDestroy(), use requireActivity().getApplicationContext() instead.)

Next, modify PhotoGalleryFragment to be a subclass of your new VisibleFragment.

Listing 28.9  Making your fragment visible (PhotoGalleryFragment.kt)

class PhotoGalleryFragment : Fragment() VisibleFragment() {
  ...
}

Run PhotoGallery and toggle background polling a couple of times. You will see a nice toast pop up (Figure 28.2).

Figure 28.2  Proof that your broadcast receiver exists

Proof that your broadcast receiver exists

Passing and receiving data with ordered broadcasts

Time to finally bring this baby home. The last piece is to ensure that your dynamically registered receiver always receives the PollWorker.ACTION_SHOW_NOTIFICATION broadcast before any other receivers and that it modifies the broadcast to indicate that the notification should not be posted.

Right now you are sending your own personal private broadcast, but so far you only have one-way communication (Figure 28.3).

Figure 28.3  Regular broadcast intents

Regular broadcast intents

This is because a regular broadcast intent is conceptually received by everyone at the same time. In reality, because onReceive(…) is called on the main thread, your receivers are not actually executed concurrently. However, it is not possible to rely on them being executed in any particular order or to know when they have all completed execution. As a result, it is a hassle for the broadcast receivers to communicate with each other or for the sender of the intent to receive information from the receivers.

You can implement predictably ordered communication using an ordered broadcast intent (Figure 28.4). Ordered broadcasts allow a sequence of broadcast receivers to process a broadcast intent in order.

Figure 28.4  Ordered broadcast intents

Ordered broadcast intents

On the receiving side, this looks mostly the same as a regular broadcast. But you get an additional tool: a set of functions used to change the intent being passed along the chain. Here, you want to cancel the notification. This can be communicated by use of a simple integer result code by setting resultCode to Activity.RESULT_CANCELED.

Modify VisibleFragment to tell the sender of SHOW_NOTIFICATION whether the notification should be posted. This information will also be sent to any other broadcast receivers along the chain.

Listing 28.10  Sending a simple result back (VisibleFragment.kt)

private const val TAG = "VisibleFragment"

abstract class VisibleFragment : Fragment() {

    private val onShowNotification = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            Toast.makeText(requireActivity(),
                    "Got a broadcast:" + intent.getAction(),
                    Toast.LENGTH_LONG)
                    .show()
            // If we receive this, we're visible, so cancel
            // the notification
            Log.i(TAG, "canceling notification")
            resultCode = Activity.RESULT_CANCELED
        }
    }
    ...
}

Because all you need to do is signal yes or no here, you only need the result code. If you needed to pass more complicated data, you could set resultData or call setResultExtras(Bundle?). And if you wanted to set all three values, you could call setResult(Int, String?, Bundle?). Each subsequent receiver will be able to see or even modify these values.

For those functions to do anything useful, your broadcast needs to be ordered. Write a new function to send an ordered broadcast in PollWorker. This function will package up a Notification invocation and send it out as a broadcast. Update doWork() to call your new function and, in turn, send out an ordered broadcast instead of posting the notification directly to the NotificationManager.

Listing 28.11  Sending an ordered broadcast (PollWorker.kt)

class PollWorker(val context: Context, workerParams: WorkerParameters) :
    Worker(context, workerParams) {

    override fun doWork(): Result {
        ...
        val resultId = items.first().id
        if (resultId == lastResultId) {
            Log.i(TAG, "Got an old result: $resultId")
        } else {
            ...
            val notification = NotificationCompat
                .Builder(context, NOTIFICATION_CHANNEL_ID)
                ...
                .build()

            val notificationManager = NotificationManagerCompat.from(context)
            notificationManager.notify(0, notification)

            context.sendBroadcast(Intent(ACTION_SHOW_NOTIFICATION), PERM_PRIVATE)

            showBackgroundNotification(0, notification)
        }

        return Result.success()
    }

    private fun showBackgroundNotification(
        requestCode: Int,
        notification: Notification
    ) {
        val intent = Intent(ACTION_SHOW_NOTIFICATION).apply {
            putExtra(REQUEST_CODE, requestCode)
            putExtra(NOTIFICATION, notification)
        }

        context.sendOrderedBroadcast(intent, PERM_PRIVATE)
    }

    companion object {
        const val ACTION_SHOW_NOTIFICATION =
            "com.bignerdranch.android.photogallery.SHOW_NOTIFICATION"
        const val PERM_PRIVATE = "com.bignerdranch.android.photogallery.PRIVATE"
        const val REQUEST_CODE = "REQUEST_CODE"
        const val NOTIFICATION = "NOTIFICATION"
    }
}

Context.sendOrderedBroadcast(Intent, String?) behaves much like sendBroadcast(…), but it will guarantee that your broadcast is delivered to each receiver one at a time. The result code will be initially set to Activity.RESULT_OK when this ordered broadcast is sent.

Update NotificationReceiver so that it posts the notification to the user.

Listing 28.12  Implementing your result receiver (NotificationReceiver.kt)

private const val TAG = "NotificationReceiver"

class NotificationReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        Log.i(TAG, "received broadcast: ${i.action} result: $resultCode")
        if (resultCode != Activity.RESULT_OK) {
            // A foreground activity canceled the broadcast
            return
        }

        val requestCode = intent.getIntExtra(PollWorker.REQUEST_CODE, 0)
        val notification: Notification =
            intent.getParcelableExtra(PollWorker.NOTIFICATION)

        val notificationManager = NotificationManagerCompat.from(context)
        notificationManager.notify(requestCode, notification)
    }
}

To ensure that NotificationReceiver receives the broadcast after your dynamically registered receiver (so it can check whether it should post the notification to NotificationManager), you need to set a low priority for NotificationReceiver in the manifest. Give it a priority of -999 so that it runs last. This is the lowest user-defined priority possible (-1000 and below are reserved).

Listing 28.13  Prioritizing the notification receiver (manifests/AndroidManifest.xml)

<receiver ... >
    <intent-filter android:priority="-999">
        <action
            android:name="com.bignerdranch.android.photogallery.SHOW_NOTIFICATION" />
    </intent-filter>
</receiver>

Run PhotoGallery. You should see that notifications no longer appear when you have the app in the foreground. (You can toggle polling off and on to trigger your PollWorker to be run by WorkManager.)

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
35.175.174.36