Android四大组件之BroadCastReceiver

1. 基本概念

在Android 中,Broadcast 是一种广泛运用的在应用程序之间传输信息的机制。而BroadcastReceiver 是对发送出来的Broadcast 进行过滤接受并响应的一类组件,是Android 四大组件之一。

广播接收者(BroadcastReceiver)用于接收广播的,广播的发送是通过调用sendBroadcast(Intent),sendOrderedBroadcast(Intent)来实现的。通常一个广播可以被多个广播接收者所接收。

广播被分为两种不同的类型:“普通广播(Normal Broadcasts)”也叫无序广播和“有序广播(OrderedBroadcasts)”。

1、普通广播是完全异步(就是不会被某个广播接收者终止)的,可以在同一时刻(逻辑上)被所有接收者接收到(其实被接收者接收到也是由顺序的,接收者配置的优先级越高,越先接收到,也就是说广
播接收者的优先级对于无序广播也是有用的),消息传递的效率比较高,但缺点是:接收者不能将处理结果传递给下一个接收者,并且无法终止广播的传播。

2、有序广播是按照接收者声明的优先级别,被接收者依次接收广播。如:A 接收者的级别高于B,B的级别高于C,那么,广播先传给A,再传给B,最后传给C 。在传递的过程中如果有某个接收者终止(abortBroadCast)了该广播,那么后面的接收者就接收不到该广播。

3、广播接收者属于四大组件之一,因此通常需要AndroidManifest.xml 中进行注册,优先级别声明在intent-filter 元素的android:priority 属性中,数越大优先级别越高,取值范围:-1000 到1000,优先级别也可以调用IntentFilter 对象的setPriority()进行设置。

4、有序广播的接收者可以终止广播的传播,广播的传播一旦终止,后面的接收者就无法接收到广播,有序广播的接收者可以将数据传递给下一个接收者,如:A 得到广播后,可以往它的结果对象中存入数据,当广播传给B 时,B 可以从A 的结果对象中得到A 存入的数据。

5、Context.sendBroadcast() 发送的是普通广播,所有订阅者都有机会获得并进行处理。

6、Context.sendOrderedBroadcast() 发送的是有序广播,系统会根据接收者声明的优先级别按顺序逐个执行接收者,前面的接收者有权终止广播(BroadcastReceiver.abortBroadcast()),如果广播被前面的接收者终
止, 后面的接收者就再也无法获取到广播。对于有序广播, 前面的接收者可以将数据通过setResultExtras(Bundle)方法存放进结果对象,然后传给下一个接收者,下一个接收者通过代码:Bundle bundle = getResultExtras(true))可以获取上一个接收者存入在结果对象中的据。

2. Android 系统常见的广播

Android 为了将系统运行时的各种“事件”通知给其他应用(或者说通知给我们程序员,让我们程序员好做出相应的反应。举个生活中的例子:比如我们坐火车,当前方到达某站的时候,火车乘务员会给所有乘客发送即将到站的广播,这样乘客收到广播后就可以提前准备下车),因此内置了多种广播,比如:系统电量的改变、屏幕的锁屏、网络状态的改变、接收到新的短信、拨打电话事件、sdcard 的挂载和移除、应用的安装和卸载等等。比如我们开发的在线播放视频类的APP,那么我们就有必要监听网络转态改变的事件广播,如果用户的网络状态从wifi 改变为了4G 上网,那么应该提示用户是否使用4G 网络继续播放视频,如果不提示用户,那么就可能导致用户流量被大量使用,一会儿功夫,用户可能就要停机了。

接下来我们会用4 个案例来演示广播接收者的使用。

2.1 案例-IP 拨号器

需求分析

什么是IP 拨号服务?我们为什么要用IP 服务?所谓的IP 拨号就是通过接入数据网络来传播语音信息。IP 拨号的目的在于转接至其他频道,减少话费等用处。移动17951,联通17911,打长途时在电话号码前加上这个就便宜了,如果你的手机上有这个键的话,那么打电话时输入长途电话号码后,直接按那个键就拨出去了,它会自动加上IP。通俗的说就是打长途便宜。

例如手机拨打长途电话:
移动拨区号+电话号=0.25/分市话+0.7/分长途=0.95/分;
移动拨17951+区号+电话号=0.25/分市话+0.3/分长途=0.55/分。

BroadcastReceiver

了解了IP 拨号的用途之后,接下来,我们通过程序在用户拨出去的号码前自动加上一个IP 号码,为用户省钱。

之所以能实现这样的功能,是因为拨号的时候Android 系统会发送一个有序广播,该广播中携带了用户拨打的号码,我们通过注册广播接收者就可以获取到该广播,同时将该广播中的数据进行修改。从而实现了用户号码自动加IP 号的功能。

为了能让用户自己决定IP 号码,我们需要一个界面(如图1-2),让那个用户输入IP 号码,然后将该IP 号码保存到SharedPreferences 中。

IP 拨号器界面

布局文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/et_ip"
android:hint="请输入IP 号码,默认17951"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:onClick="saveIP"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="保存"
/>
</LinearLayout>

实现代码
在该案例中总共用到了两个类一个是MainActivity.java 负责让用户输入IP 号码,另外一个是自定义的广播接收者IPCallerReceiver 负责监听用户的拨打电话事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import android.os.Bundle;
import android.app.Activity;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
//保存用户的IP 号码
public class MainActivity extends Activity {
private EditText et_ip;
private SharedPreferences sp;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//文本编辑控件
et_ip = (EditText) findViewById(R.id.et_ip);
//获取sp 对象
sp = getSharedPreferences("config", MODE_PRIVATE);
}
//保存IP 号码
public void saveIP(View view){
String ipNum = et_ip.getText().toString().trim();
//如果为空则保存默认值
if (TextUtils.isEmpty(ipNum)) {
sp.edit().putString("ip", "17951").commit();
}else {
sp.edit().putString("ip", ipNum).commit();
}
Toast.makeText(this, "IP 号码保存成功", Toast.LENGTH_SHORT).show();
}
}

编写自定义广播接收者需要自定义一个类然后继承系统提供的BroadCastReceiver 类,然后覆写抽象方法onReceive。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.itheima.android.ipcaller;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;
//自定义广播接收者
public class IPCallerReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//获取数据
String resultData = getResultData();
Log.d("tag", "接收到广播:"+resultData);
//从SharedPreferences 中获取用户保存的IP 号码
SharedPreferences sp =
context.getSharedPreferences("config", Context.MODE_PRIVATE);
String ipNum = sp.getString("ip", "17951");
if (!TextUtils.isEmpty(ipNum)) {
//修改数据
resultData = ipNum+resultData;
}
//将修改后的数据设置出去
setResultData(resultData);
}
}

在清单文件中进行注册

广播是Android 四大组件之一,因此需要在AndroidManifest.xml 中进行注册。同时监听用户的拨打电话行为也属于侵犯用户隐私的行为,因此需要添加权限。

注册广播

1
2
3
4
5
<receiver android:name="com.itheima.android.ipcaller.IPCallerReceiver">
<intent-filter >
<action android:name="android.intent.action.NEW_OUTGOING_CALL"></action>
</intent-filter>
</receiver>

大家可以发现广播接收者的注册也需要通过intent-filter 来监听特定的广播,如果是监听Android 系统的,那么在action 中就需要配置系统提供的常量。如果监听自定义发送的广播,那么就需要配置自定义广播设置的action。

声明权限

1
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>

2.2 案例-短信监听器

系统接收到短信时会将该事件以有序广播(部分自定义的ROM 可能已经修改了这个策略,比如:小米的MIUI 系统) 的形式发送出去, 因此我们只需要自定义一个BroadCastReceiver 监听该广播(android.provider.Telephony.SMS_RECEIVED)即可监听到短信的到来。由于该广播是有序的,因此如果将我们自定义的BroadCastReceiver 配置了较高的优先级,那么我们就能先于系统短信app 接收到该广播,
然后终止该广播,从而就实现了短信拦截功能。

通过该案例我们可以学到:

  • 什么是有序广播?
  • 如何终止有序广播
  • 如何从广播中获取短信
  • 广播的优先级概念

在该案例中我们要做一个类似短信黑名单的应用,主界面提供一个EditText 和一个Button,让用户输入一个“黑名单”,点击保存之后,如果该号码发短信过来,那么我们的应用就将其拦截

BroadcastReceiver

2.3 案例-监听应用的安装和卸载

在Android 系统中,安装应用和卸载应用事件也都会发送特定的广播,我们可以通过监听这些广播间接获取到用户新安装了什么软件,卸载了哪些软件,进而可以统计用户的偏好,或统计某个软件的存留率。

需求很简单,监听应用的安装和卸载,并将其报名打印出来即可。
该应用不需要界面,代码也很简单,只需要一个自定义广播接收这就可以了。

3. 发送无序广播

4. 发送有序广播

5. 特殊的广播接收者-锁屏与解屏

拦截短信的广播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private class InnerSmsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
System.out.println("InnerSmsReceiver");
// 获取到短信
Object[] objects = (Object[]) intent.getExtras().get("pdus");
for (Object obj : objects) {
SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) obj);
// 获取到短信内容
String body = smsMessage.getMessageBody();
// 获取到电话号码
String phone = smsMessage.getDisplayOriginatingAddress();
// 根据电话号码查询拦截的模式
String mode = dao.findNumberMode(phone);
/**
* 黑名单的拦截模式1 全部拦截(电话拦截+ 短信拦截) 2 电话拦截3 短信拦截
*/
if ("1".equals(mode) || "3".equals(mode)) {
System.out.println("被哥拦截了");
//往短信拦截数据库里面添加数据
abortBroadcast();
}
/**
* 根据内容拦截(智能拦截)
*/
if (body.contains("xue sheng mei")) {
System.out.println("学生妹被拦截了");
abortBroadcast();
}
}
}
}

注册静态广播

1
2
3
4
5
6
receiver = new InnerSmsReceiver();
IntentFilter filter = new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
// 设置优先级
filter.setPriority(2147483647);
// 注册一个短信监听的广播
registerReceiver(receiver, filter);

反注册广播,防止内存泄露

1
2
3
4
5
6
7
8
9
public void onDestroy() {
super.onDestroy();
// 反注册
unregisterReceiver(receiver);
receiver = null;
// 当不用了。设置为null
mTelephonyManager.listen(listener, PhoneStateListener.LISTEN_NONE);
listener = null;
}

注册广播并设置优先级

1
2
3
4
5
6
7
<!-- 拦截黑名单信息-->
<receiver
android:name="com.itheima.mobilesafe_sh2.receiver.InnerSmsReceiver " >
<intent-filter android:priority="1000" >
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>

案例1:IP拨号器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CallReceiver extends BroadcastReceiver {
//接收到广播时就会调用
@Override
public void onReceive(Context context, Intent intent) {
//添加IP线路
//在打电话广播中,会携带拨打的电话的号码,通过以下代码获取到
String number = getResultData();
if(number.startsWith("0")){
SharedPreferences sp = context.getSharedPreferences("ip", Context.MODE_PRIVATE);
String ipNumber = sp.getString("ipNumber", "");
//把IP线路号码添加至用户拨打号码的前面
number = ipNumber + number;
//把新的号码重新放入广播中
setResultData(number);
abortBroadcast();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void click(View v){
EditText et = (EditText) findViewById(R.id.et);
SharedPreferences sp = getSharedPreferences("ip", MODE_PRIVATE);
sp.edit().putString("ipNumber", et.getText().toString()).commit();
}
}

案例2:短信防火墙

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SmsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//拿到短信的信息
//短信内容封装在intent中
Bundle bundle = intent.getExtras();
//以pdus为键,取出一个object数组,数组中的每一个元素,都是一条短信
Object[] objects = (Object[]) bundle.get("pdus");
//拿到广播中的所有短信
for (Object object : objects) {
//通过pdu来构造短信
SmsMessage sms = SmsMessage.createFromPdu((byte[])object);
if(sms.getOriginatingAddress().equals("138438")){
//阻止其他广播接收者收到这条广播
abortBroadcast();
// SmsManager.getDefault().sendTextMessage(sms.getOriginatingAddress(), null, "你是个好人", null, null);
}
// System.out.println(sms.getMessageBody());
}
}
}

案例3:监听SD卡状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SDStatusReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//判断收到的到底是什么广播
String action = intent.getAction();
if("android.intent.action.MEDIA_MOUNTED".equals(action)){
Toast.makeText(context, "SD卡可用", 0).show();
}
else if("android.intent.action.MEDIA_REMOVED".equals(action)){
Toast.makeText(context, "SD卡拔出", 0).show();
}
else if("android.intent.action.MEDIA_UNMOUNTED".equals(action)){
Toast.makeText(context, "SD卡不可用", 0).show();
}
}
}

案例4:手机勒索软件

1
2
3
4
5
6
7
8
9
10
11
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 启动Activity,实现开机自动启动勒索软件
Intent it = new Intent(context, MainActivity.class);
//创建任务栈存放启动的Activity
it.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(it);
}
}

案例5:监控应用的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class APPStatusReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// TODO Auto-generated method stub
String action = intent.getAction();
Uri uri = intent.getData();
if("android.intent.action.PACKAGE_ADDED".equals(action)){
Toast.makeText(context, uri.toString() + "被安装了", 0).show();
}
if("android.intent.action.PACKAGE_REPLACED".equals(action)){
Toast.makeText(context, uri.toString() + "被升级了", 0).show();
}
if("android.intent.action.PACKAGE_REMOVED".equals(action)){
Toast.makeText(context, uri.toString() + "被卸载了", 0).show();
}
}
}

案例6:发送自定义广播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void click(View v){
//发送自定义广播
Intent intent = new Intent();
//广播中的action也是自定义的
intent.setAction("com.itheima.zdy");
sendBroadcast(intent);
}
}
坚持原创技术分享,您的支持将鼓励我继续创作!