本章需求:首先,让应用轮询新结果并在有所发现时及时通知用户,即使用户重启设备后还没有打开过应用。其次,保证用户在使用应用时不出现新结果通知。

 

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 的广播管理类;但上述第三方类库用起来更为灵活和方便。