Filtering Foreground Notifications

With that sharp corner filed down a bit, let’s turn to another imperfection in PhotoGallery. Your notifications work great, but they are sent even when the user already has the application open.

You can fix this problem with broadcast intents, too. But they will work in a completely different way.

First, you will send (and receive) your own custom broadcast intent (and ultimately will lock it down so it can be received only by components in your application). Second, you will register a receiver for your broadcast dynamically in code, rather than in the manifest. Finally, you will send an ordered broadcast to pass data along a chain of receivers, ensuring a certain receiver is run last. (You do not know how to do all this yet, but you will by the time you are done.)

Sending broadcast intents

The first part is straightforward: You need to send your own broadcast intent. Specifically, you will send a broadcast 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.

Add these items in PollService.

Listing 29.6  Sending a broadcast intent (PollService.java)

public class PollService extends IntentService {
    private static final String TAG = "PollService";

    private static final long POLL_INTERVAL_MS = TimeUnit.MINUTES.toMillis(15);

    public static final String ACTION_SHOW_NOTIFICATION =
            "com.bignerdranch.android.photogallery.SHOW_NOTIFICATION";
    ...
    @Override
    protected void onHandleIntent(Intent intent) {
        ...
        String resultId = items.get(0).getId();
        if (resultId.equals(lastResultId)) {
            Log.i(TAG, "Got an old result: " + resultId);
        } else {
            ...
            NotificationManagerCompat notificationManager =
                    NotificationManagerCompat.from(this);
            notificationManager.notify(0, notification);

            sendBroadcast(new Intent(ACTION_SHOW_NOTIFICATION));
        }

        QueryPreferences.setLastResultId(this, resultId);
    }
    ...
}

Now your app will send out a broadcast every time new search results are available.

Creating and registering a dynamic receiver

Next, you need a receiver for your ACTION_SHOW_NOTIFICATION broadcast intent.

You could write a standalone broadcast receiver, like StartupReceiver, and register it in the manifest. But that would not be ideal in this case. Here, you want PhotoGalleryFragment to receive the intent only while it is alive. A standalone receiver declared in the manifest would always receive the intent and would need some other way of knowing that PhotoGalleryFragment is alive (which is not easily achieved in Android).

The solution is to use a dynamic broadcast receiver. A dynamic receiver is registered in code, not in the manifest. You register the receiver by calling registerReceiver(BroadcastReceiver, IntentFilter) and unregister it by calling unregisterReceiver(BroadcastReceiver). The receiver itself is typically defined as an inner instance, like a button-click listener. However, since you need the same instance in registerReceiver(…) and unregisterReceiver(BroadcastReceiver), 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 30.)

Listing 29.7  A receiver of VisibleFragment’s own (VisibleFragment.java)

public abstract class VisibleFragment extends Fragment {
    private static final String TAG = "VisibleFragment";

    @Override
    public void onStart() {
        super.onStart();
        IntentFilter filter = new IntentFilter(PollService.ACTION_SHOW_NOTIFICATION);
        getActivity().registerReceiver(mOnShowNotification, filter);
    }

    @Override
    public void onStop() {
        super.onStop();
        getActivity().unregisterReceiver(mOnShowNotification);
    }


    private BroadcastReceiver mOnShowNotification = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(getActivity(),
                    "Got a broadcast:" + intent.getAction(),
                    Toast.LENGTH_LONG)
                 .show();
        }
    };
}

Note that to pass in an IntentFilter, you have to create one in code. Your IntentFilter 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 method, you call Context.unregisterReceiver(BroadcastReceiver) in the corresponding shutdown method. 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/unregister in Fragment.onCreate(Bundle) and Fragment.onDestroy(), use getActivity().getApplicationContext() instead.)

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

Listing 29.8  Making your fragment visible (PhotoGalleryFragment.java)

public class PhotoGalleryFragment extends Fragment VisibleFragment {
    ...
}

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

Figure 29.4  Proof that your broadcast exists

Screenshot shows Photogallery with a popup message reading, Got a com.bignerdranch.android.photogallery.SHOW_NOTIFICATION.

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.

You can preclude these unauthorized intrusions into your personal business in a couple of ways. One way is to declare in your manifest that the receiver is internal to your app by adding an android:exported="false" attribute to your receiver tag. This will prevent it from being visible to other applications on the system.

Another way is to create your own permission by adding a permission tag to your AndroidManifest.xml. This is the approach you will take for PhotoGallery.

Declare and acquire your own permission in AndroidManifest.xml.

Listing 29.9  Adding a private permission (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="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <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 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 needs to appear in three more places and must be identical in each place. You would be wise to copy and paste it rather than typing it out by hand.

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

Listing 29.10  Sending with a permission (PollService.java)

public class PollService extends IntentService {
    ...
    public static final String ACTION_SHOW_NOTIFICATION =
            "com.bignerdranch.android.photogallery.SHOW_NOTIFICATION";
    public static final String PERM_PRIVATE =
            "com.bignerdranch.android.photogallery.PRIVATE";

    public static Intent newIntent(Context context) {
        return new Intent(context, PollService.class);
    }
    ...
    @Override
    protected void onHandleIntent(Intent intent) {
        ...
        String resultId = items.get(0).getId();
        if (resultId.equals(lastResultId)) {
            Log.i(TAG, "Got an old result: " + resultId);
        } else {
            ...
            notificationManager.notify(0, notification);

            sendBroadcast(new Intent(ACTION_SHOW_NOTIFICATION), PERM_PRIVATE);
        }

        QueryPreferences.setLastResultId(this, resultId);
    }
    ...
}

To use your permission, you pass it as a parameter to sendBroadcast(…). Using the permission here ensures that any application must use that same permission to receive the intent you are sending.

What about your broadcast receiver? Someone could create a broadcast intent to trigger it. You can fix that by passing in your permission in registerReceiver(…), too.

Listing 29.11  Setting permissions on a broadcast receiver (VisibleFragment.java)

public abstract class VisibleFragment extends Fragment {
    ...
    @Override
    public void onStart() {
        super.onStart();
        IntentFilter filter = new IntentFilter(PollService.ACTION_SHOW_NOTIFICATION);
        getActivity().registerReceiver(mOnShowNotification, filter,
                PollService.PERM_PRIVATE, null);
    }
    ...
}

Now, your app is the only app that can trigger that receiver.

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.

Table 29.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.RECEIVE_BOOT_COMPLETED uses this permission level, and so does the permission that lets your app vibrate the user’s device.
dangerous This is for everything 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 permissions, 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. (For more about how that works, see Chapter 33.)
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 do not need to use it.

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 PollService.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 29.5).

Figure 29.5  Regular broadcast intents

Figure shows 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 their 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 two-way communication using an ordered broadcast intent (Figure 29.6). Ordered broadcasts allow a sequence of broadcast receivers to process a broadcast intent in order. They also allow the sender of a broadcast to receive results from the broadcast’s recipients by passing in a special broadcast receiver, called the result receiver.

Figure 29.6  Ordered broadcast intents

Figure shows 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 methods used to change the return value of your receiver. Here, you want to cancel the notification. This can be communicated by use of a simple integer result code. You will use the setResultCode(int) method to set the result code 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 29.12  Sending a simple result back (VisibleFragment.java)

public abstract class VisibleFragment extends Fragment {
    private static final String TAG = "VisibleFragment";
    ...
    private BroadcastReceiver mOnShowNotification = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(getActivity(),
                    "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");
            setResultCode(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 return more complicated data, you could use setResultData(String) or setResultExtras(Bundle). And if you wanted to set all three values, you could call setResult(int, String, Bundle). Once your return values are set, every subsequent receiver will be able to see or modify them.

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

Listing 29.13  Sending an ordered broadcast (PollService.java)

public static final String PERM_PRIVATE =
        "com.bignerdranch.android.photogallery.PRIVATE";
public static final String REQUEST_CODE = "REQUEST_CODE";
public static final String NOTIFICATION = "NOTIFICATION";
...
@Override
protected void onHandleIntent(Intent intent) {
    ...
    String resultId = items.get(0).getId();
    if (resultId.equals(lastResultId)) {
        Log.i(TAG, "Got an old result: " + resultId);
    } else {
        Log.i(TAG, "Got a new result: " + resultId);
        ...

        Notification notification = ...;

        NotificationManagerCompat notificationManager =
                NotificationManagerCompat.from(this);
        notificationManager.notify(0, notification);

        sendBroadcast(new Intent(ACTION_SHOW_NOTIFICATION), PERM_PRIVATE);
        showBackgroundNotification(0, notification);
    }

    QueryPreferences.setLastResultId(this, resultId);
}

private void showBackgroundNotification(int requestCode, Notification notification) {
    Intent i = new Intent(ACTION_SHOW_NOTIFICATION);
    i.putExtra(REQUEST_CODE, requestCode);
    i.putExtra(NOTIFICATION, notification);
    sendOrderedBroadcast(i, PERM_PRIVATE, null, null,
            Activity.RESULT_OK, null, null);
}

Context.sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle) has five additional parameters beyond the ones you used in sendBroadcast(Intent, String). They are, in order: a result receiver, a Handler to run the result receiver on, and initial values for the result code, result data, and result extras for the ordered broadcast.

The result receiver is a special receiver that runs after all the other recipients of your ordered broadcast intent. In other circumstances, you would be able to use the result receiver to receive the broadcast and post the notification object. Here, though, that will not work. This broadcast intent will often be sent right before PollService dies. That means that your broadcast receiver might be dead, too.

Thus, your final broadcast receiver will need to be a standalone receiver, and you will need to enforce that it runs after the dynamically registered receiver by different means.

First, create a new BroadcastReceiver subclass called NotificationReceiver. Implement it as follows:

Listing 29.14  Implementing your result receiver (NotificationReceiver.java)

public class NotificationReceiver extends BroadcastReceiver {
    private static final String TAG = "NotificationReceiver";

    @Override
    public void onReceive(Context c, Intent i) {
        Log.i(TAG, "received result: " + getResultCode());
        if (getResultCode() != Activity.RESULT_OK) {
            // A foreground activity cancelled the broadcast
            return;
        }

        int requestCode = i.getIntExtra(PollService.REQUEST_CODE, 0);
        Notification notification = (Notification)
                i.getParcelableExtra(PollService.NOTIFICATION);

        NotificationManagerCompat notificationManager =
                NotificationManagerCompat.from(c);
        notificationManager.notify(requestCode, notification);
    }
}

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

Also, since this receiver is only used by your application, you do not need it to be externally visible. Set android:exported="false" to keep this receiver to yourself.

Listing 29.15  Registering the notification receiver (AndroidManifest.xml)

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

Run PhotoGallery and toggle background polling a couple of times. You should see that notifications no longer appear when you have the app in the foreground. (If you have not already done so, change PollService.POLL_INTERVAL_MS to 60 seconds so that you do not have to wait 15 minutes to verify that notifications still work in the background.)

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

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