本章需求:首先,让应用轮询新结果并在有所发现时及时通知用户,即使用户重启设备后还没有打开过应用。其次,保证用户在使用应用时不出现新结果通知。
1. 一般intent和broadcast intent
许多系统组件需要知道某些事件的发生(WIFI信号时有时无,电话的呼入等),为满足这样的需求,Andorid提供了broadcast intent 组件。
broadcast intent的工作原理类似于之前学过的intent,但不同的是broadcast intent可以被多个叫做broadcast receiver的组件接收、
2. 接受系统broadcast : 重启后唤醒
2.1 standalone receiver
standalone receiver 是一个在manifest配置文件中声明的broadcast receiver。即使应用进程已消灭,standalone receiver也可以被激活。(还有一种是可以同fragemt或activity的生命周期绑定的dynamic receiver)
broadcast receiver必须在系统中登记后才能发挥作用,如果不登记,系统就不知道该向哪里发送intent,broadcast receiver的onReceiver()方法也就得不到预定的调用了。
要登记broad receiver,首先要创建它:
public class StartupReceiver extends BroadcastReceiver {
private static final String TAG = "StartupReceiver";
@Override
public void onReceive(Context context, Intent intent) { //onReceiver是在主线程中执行的
Log.i(TAG, "Received broadcast intent: " + intent.getAction());
boolean isOn = QueryPreferences.isAlarmOn(context);
PollService.setServiceAlarm(context, isOn);
}
}
broadcast receiver是接受intent的组件,当有intent发送给StartupReceiver时,它 的onReceive()方法会被调用。
然后在AndroidManifest.xml文件中声明:
<usespermissionandroid:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<receiver android:name=".StartupReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
完成声明后,即使app并未运行,只要有匹配的broadcast intent发来,broadcast receiver就会醒来接受,一收到intent,broadcast receiver的onReceive(Context. Intent)方法即开始执行,随后会被销毁。
3. 过滤前台通知消息
PhotoGallery应用的另一缺陷,通知消息虽然很有用,但应用开着的时候不应该受到通知消息。
解决方式:
首先,我们发送(或接收)定制版broadcast intent(最后会锁定它,只允许PhotoGallery应用部件接收它)。其次,不再使用manifest文件,改用代码为broadcast intent动态登记receiver。(动态注册的receiver与fragment进行绑定,收到广播时说明是在app中) 最后,发送一个有序broadcast在一组receiver中传递数据,借此保证最后才运行某个receiver(最后的receiver决定显不显示通知,这个receiver是静态注册的)。
3.1 发送broadcast intent
要发送broadcast intent,需要创建一个intent,并传入sendBroadcast(intent)方法即可。
public static final String ACTION_SHOW_NOTIFICATION = "com.bignerdranch.android.photogallery.SHOW_NOTIFICATION";
sendBroadcast(new Intent(ACTION_SHOW_NOTIFICATION));
3.2 动态broadcast receiver
动态broadcast receiver是在代码中,而不是在配置文件中完成登记声明。要在代码中登记,可调用registerReceiver(BroadcastReceiver, IntentFliter)方法,取消登记时,则调用unregiseterReceiver
(BroadcastReceiver)方法。receiver自身通常被定义为一个内部类实例,如同一个按钮点击监听器。在registerReceiver()和unregisterReceiver()方法的BroadcastReceiver需要的是同一个实例、
我们要只在应用开启的时候接受发过来的广播过滤,就不能在 manifest 中声明一个过滤器,而是要动态地建立一个广播接收器。我们在这里建立一个用于隐藏前台通知的通用 fragment 子类:
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,
PollService.PERM_PRIVATE, null);
}
@Override
public void onStop() {
super.onStop();
getActivity().unregisterReceiver(mOnShowNotification);
}
private BroadcastReceiver mOnShowNotification = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 如果接收到广播,说明应用正在前台,所以把 ResultCode 更改掉
Log.i(TAG, "canceling notification");
setResultCode(Activity.RESULT_CANCELED);
}
};
}
3.3 使用私有权限
使用动态broadcast receiver存在一个问题,即系统中的任何应用均可监听并触发我们的receiver。
有两种办法可以阻止应用闯入我们的私人领域,一种办法是在mainfest配置文件里给receiver标签添加一个android:exported= “false”属性,声明它仅限应用内部使用。
另外,也可以创建自己的使用权限,可以通过在AndroidManifest.xml中添加一个permission标签来完成:
<permission android:name="com.bignerdranch.android.photogallery.PRIVATE"
android:protectionLevel="signature" />
<uses-permission android:name="com.bignerdranch.android.photogallery.PRIVATE" />
要使用权限,须将其作为参数传入sendBroadcast(),有了这个权限,所有应用都必须使用同样的权限才能接受我们发送的intent。
要怎么保护我们的broad receiver呢?其他应用可通过创建自己的broadcast intent来触发它。同样,在 registerReceiver(...) 方法中传入自定义权限就能解决该问题:
public abstract class VisibleFragment extends Fragment { ... @Override public void onStart() { super.onStart(); IntentFilter filter = newIntentFilter(PollService.ACTION_SHOW_NOTIFICATION); getActivity().registerReceiver(mOnShowNotification, filter, PollService.PERM_PRIVATE, null); } ... }
3.3.1 深入学习安全级别
自定义权限必须指定 android:protectionLevel 属性值。Android根据 protectionLevel 属性值确定自定义权限的使用方式。在PhotoGallery应用中,我们使用的 protectionLevel 是signature 。signature 安全级别表明,如果其他应用需要使用我们的自定义权限,则必须使用和当前应用相同的key做签名认证。对于仅限应用内部使用的权限,选择 signature 安全级别比较合适。既然其他开发者没有相同的key,自然也就无法接触到权限保护的东西。此外,有了自己的key,将来还可用于我们开发的其他应用中。
3.4 使用有序broadcast
如果想让程序在打开时不发送出通知,就不能再让服务来发出通知了,因为它无法知道前台的运行状态。所以我们让 PollService 发送一个有序广播。
public static final String REQUEST_CODE = "REQUEST_CODE"; public static final String NOTIFICATION = "NOTIFICATION"; 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); }
有序广播是按照优先级发送的,先发送给优先级高的接收器,再发给优先级低的接收器。因为在应用结束后也要发出通知,显然我们发出通知的广播接收器是需要声明在 manifest 文件中的。
内部实现如下:
public class NotificationReceiver extends BroadcastReceiver { private static final String TAG = "NotificaitonReceiver"; @Override public void onReceive(Context context, Intent intent) { Log.i(TAG, "received result: " + getResultCode()); if (getResultCode() != Activity.RESULT_OK) { // PollService 发出的 intent 带的结果码是 RESULT_OK // 如果接到的不是,说明应用在前台,将结果码修改了 return; } // 如果没有 return,说明应用不在前台,就可以发出通知了。 int requestCode = intent.getIntExtra(PollService.REQUEST_CODE, 0); Notification notification = (Notification) intent.getParcelableExtra(PollService.NOTIFICATION); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); notificationManager.notify(requestCode, notification); } }
<receiver android:name=".NotificationReceiver" android:exported="false"> <!-- 在这里将优先级设为最低,即 -999 --> <intent-filter android:priority="-999"> <action android:name="com.kniost.photogallery.SHOW_NOTIFICATION" /> </intent-filter> </receiver>
3.5 receiver与长时间运行任务
如不想受限与主线程的时间限制,希望broadcast intent触发一个长时间运行任务,该如何做呢?
- 将任务交给服务处理,然后通过broadcast receiver启动服务。
- 使用BroadcastReceiver.getAsync()方法。该方法返回一个 BroadcastReceiver.PendingResult 对象,随后可使用该对象提供结果。因此,可将 PendingResult 交给AsyncTask 去执行长时运行的任务,然后再调用 PendingResult 的方法响应broadcast。
- goAsync() 方法的弊端是不够灵活。我们仍需快速响应broadcast(10秒内),并且与使用服务相比,没什么架构模式好选择。当然, goAsync() 方法也有明显的优势:可调用该方法设置有序broadcast的结果。
3.6 使用EventBus
broadcast intent可实现系统内全局性的消息传递。如果仅需要应用内的消息事件广播,该怎
么做呢?答案是使用事件总线(event bus)。事件总线的设计思路就是,提供一个应用内的部件可以订阅的共享总线或数据流。事件一旦
发布到总线上,各订阅部件就会被激活并执行相应的回调代码。由greenrobot出品的EventBus是目前广为人知的一个第三方事件总线库。
为实现在应用内发送broadcast intent,Android自己也提供了一个叫作 LocalBroadcast-
Manager 的广播管理类;但上述第三方类库用起来更为灵活和方便。