Android基础第九天
BroadCastReceiver
1. 基本概念
在Android 中,Broadcast 是一种广泛运用的在应用程序之间传输信息的机制。而BroadcastReceiver 是对发送出来的Broadcast 进行过滤接受并响应的一类组件,是Android 四大组件之一。
广播接收者(BroadcastReceiver)用于接收广播的,广播的发送是通过调用sendBroadcast(Intent)/sendOrderedBroadcast(Intent)来实现的。通常一个广播可以被多个广播接收者所接收。
广播被分为两种不同的类型:“普通广播(NormalBroadcasts)”也叫无序广播和“有序广播(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)方法存放进结果对象,然后传给下一个接收者,下一个接收者通过代码:Bundlebundle = getResultExtras(true))可以获取上一个接收者存入在结果对象中的数据。
2. 常见的广播
Android 为了将系统运行时的各种“事件”通知给其他应用(或者说通知给我们程序员,让我们程序员好做出相应的反应。举个生活中的例子:比如我们坐火车,当前方到达某站的时候,火车乘务员会给所有乘客发送即将到站的广播,这样乘客收到广播后就可以提前准备下车),因此内置了多种广播,比如:系统电量的改变、屏幕的锁屏、网络状态的改变、接收到新的短信、拨打电话事件、sdcard 的挂载和移除、应用的安装和卸载等等。比如我们开发的在线播放视频类的APP,那么我们就有必要监听网络转态改变的事件广播,如果用户的网络状态从wifi 改变为了4G 上网,那么应该提示用户是否使用4G 网络继续播放视频,如果不提示用户,那么就可能导致用户流量被大量使用,一会儿功夫,用户可能就要停机了。
3. 案例-IP拨号器
需求:
什么是IP 拨号服务?我们为什么要用IP服务?所谓的IP 拨号就是通过接入数据网络来传播语音信息。IP 拨号的目的在于转接至其他频道,减少话费等用处。移动17951,联通17911,打长途时在电话号码前加上这个就便宜了,如果你的手机上有这个键的话,那么打电话时输入长途电话号码后,直接按那个键就拨出去了,它会自动加上IP。通俗的说就是打长途便宜。
例如手机拨打长途电话:
移动拨区号+电话号=0.25/分市话+0.7/分长途=0.95/分;
移动拨17951+区号+电话号=0.25/分市话+0.3/分长途=0.55/分。
了解了IP 拨号的用途之后,接下来,我们通过程序在用户拨出去的号码前自动加上一个IP 号码,为用户省钱。
之所以能实现这样的功能,是因为拨号的时候Android系统会发送一个有序广播,该广播中携带了用户拨打的号码,我们通过注册广播接收者就可以获取到该广播,同时将该广播中的数据进行修改。从而实现了用户号码自动加IP 号的功能。
为了能让用户自己决定IP 号码,我们需要一个界面(如下图),让那个用户输入IP 号码,然后将该IP 号码保存到SharedPreferences 中。
实现步骤
1. 布局XML
<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"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="请设置您的ip号码" />
<EditText
android:hint="内容留空代表取消ip拨号"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/et_ipnumber"
/>
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="保存"
android:onClick="save"
/>
</LinearLayout>
2. 在该案例中总共用到了两个类一个是MainActivity.java 负责让用户输入IP 号码,另外一个是自定义的广播接收者OutCallerReceiver 负责监听用户的拨打电话事件。
publicclass MainActivity extends Activity {
private EditText et_ipnumber;
private SharedPreferences sp;
@Override
protectedvoid onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//文本输入框
et_ipnumber = (EditText) findViewById(R.id.et_ipnumber);
//获取sp对象
sp = getSharedPreferences("config", 0);
et_ipnumber.setText(sp.getString("ipnumber", ""));
}
/**保存IP号码 */
publicvoid save(View view){
String ipnumber = et_ipnumber.getText().toString().trim();
Editor editor = sp.edit();
editor.putString("ipnumber", ipnumber);
editor.commit();
Toast.makeText(this, "设置成功", 0).show();
}
}
编写自定义广播接收者需要自定义一个类然后继承系统提供的BroadCastReceiver 类,然后覆写抽象方法onReceive。
publicclass OutCallRecevier extends BroadcastReceiver {
@Override
publicvoid onReceive(Context context, Intentintent) {
//从SharedPreferences 中获取用户保存的IP 号码
SharedPreferences sp =context.getSharedPreferences("config", 0);
String ipnumber =sp.getString("ipnumber", "");
String number =getResultData();
if (number.startsWith("0")) {
//修改数据
setResultData(ipnumber +number);
}
}
}
3. 在清单文件中进行注册
广播是Android 四大组件之一,因此需要在AndroidManifest.xml 中进行注册。同时监听用户的拨打电话行为也属于侵犯用户隐私的行为,因此需要添加权限。
l 注册广播
<receiver android:name="com.itheima.ipdail.OutCallRecevier">
<intent-filter>
<action android:name="android.intent.action.NEW_OUTGOING_CALL"/>
</intent-filter>
</receiver>
l 声明权限
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
注意:
在使用上面案例的时候需要注意的事项比较多:
1、要想让我们的广播接收者生效,必须将我们的应用启动一次。这一点儿我们可能感觉不到,是因为当我们给模拟器安装应用的时候,系统会将其自动启动。
2、上面的代码在部分国产手机上是无法达到效果(比如我正在使用的魅族note2)的,因为国产手机已经修改了Android 系统源码,将该功能给阉割了(暂且认为是为了更安全吧)。
3、当我们的应用程序退出后,只要有广播进来,那么我们应用的进程会被系统自动启动起来,这也是我们的部分应用实现开机启动的原理,因为开机事件也会发送一个广播,我们只需要监听该广播即可。
4. 案例-短信窃听
需求:
系统接收到短信时会将该事件以有序广播(部分自定义的ROM 可能已经修改了这个策略,比如:小米的MIUI 系统)的形式发送出去,因此我们只需要自定义一个BroadCastReceiver 监听该广播(android.provider.Telephony.SMS_RECEIVED)即可监听到短信的到来。由于该广播是有序的,因此如果将我们自定义的BroadCastReceiver 配置了较高的优先级,那么我们就能先于系统短信app 接收到该广播,然后终止该广播,从而就实现了短信拦截功能。
通过该案例我们可以学到:
1、什么是有序广播?
2、如何终止有序广播
3、如何从广播中获取短信
4、广播的优先级概念
该案例很简单,我们不需要做什么界面的UI展示,只需要定义一个广播接收者去切拦截系统的短信广播即可。
实现步骤
创建一个广播接收者并在清单文件中注册指定要监听的action,和广播的优先级
publicclass SmsReceiver extends BroadcastReceiver {
@Override
publicvoid onReceive(Context context, Intentintent) {
System.out.println("短信到来了....");
// 短信的数据是pdu的数据,必须对短信的格式很了解才可以解析短信.
Object[] objs = (Object[])intent.getExtras().get("pdus");
for (Object obj : objs) {
SmsMessage smsMessage =SmsMessage.createFromPdu((byte[]) obj);
String body =smsMessage.getMessageBody();
String sender =smsMessage.getOriginatingAddress();
System.out.println("body:" + body);
System.out.println("sender:" + sender);
if (isOrderedBroadcast()) {
abortBroadcast();
}
}
}
}
清单文件注册:
<receiver android:name="com.itheima.smslistener.SmsReceiver">
<intent-filter android:priority="1000">
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>
在上面的代码中使用了比较多的新API。
1. 从Intent 中获取短信内容
Object[] objs = (Object[])intent.getExtras().get("pdus");
for(Objectobj:objs){
SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) obj);
String body =smsMessage.getMessageBody();
String sender = smsMessage.getOriginatingAddress();
System.out.println("body:"+body);
System.out.println("sender:"+sender);
abortBroadcast();
}
注:
协议数据单元(Protocol Data Unit)是指对等层次之间传递的数据单位。在Android 里用作短信的传输协议。因为短信可能会有多段(如果一条的短信内容超过一定的长度,比如在中国超过70 个汉字,那么该短信就会被拆分多段分条发送),因此上面的代码需要使用for 循环进行遍历。
2. 终止一个有序广播
abortBroadcast();
终止有序广播只需要一句代码,该代码是BroadCastReceiver类中的方法,因此这里可以直接使用。这里需要注意的是如果abortBroadCast 是在一个无序广播中执行的,那么就会报如下异常:
java.lang.RuntimeException:
BroadcastReceivertrying to return result during a non-ordered broadcast
注意:
在低版本的手机上比如Android2.3 上是不会报这样的异常的。
为了防止我们终止一个无序广播导致报异常,我们可以先判断接收到的广播类型。优化后的代码如下:
if (isOrderedBroadcast()) {
abortBroadcast();
}
isOrderedBroadcast方法是BroadCastReceiver 类提供的,用于判断当前的广播类型。返回true 为有序广播,返回false 为无序广播。
5. 发送无序广播
无序广播不可以被拦截,在高版本的系统上拦截会报异常。所有接收无序广播的广播接收者在此广播被发送时均能接收到此广播。无序广播使用Context.sendBroadcast 方法来发送:无序广播的实现比较简单,因此这里只给出核心代码。
发送无序广播代码片段:
//定义一个意图
Intent intent = new Intent();
//设置意图action
intent.setAction("com.itheima.ccav.XWLB");
//绑定数据
intent.putExtra("data", "我是无序广播的数据");
//发送无序广播
sendBroadcast(intent);
接收我们自定义无序广播的代码也很简单,主要需要两个步骤:
1. 编写自定义BroadCastReceiver
publicclass MyReceiver extends BroadcastReceiver {
@Override
publicvoid onReceive(Context context, Intentintent) {
System.out.println("哈哈哈,我们接收到了自定义的广播消息");
System.out.println(intent.getStringExtra("data"));
}
}
2. 在AndroidManifest.xml中进行注册
<receiver android:name="com.itheima.myreceiver.MyReceiver">
<intent-filter >
<action android:name="com.itheima.ccav.XWLB"/>
</intent-filter>
</receiver>
6. 发送有序广播
有序广播可以被拦截,且优先级高的接收者可以拦截优先级低的。
l 广播接收者的优先级的推荐取值范围是: 1000(最高) ~ -1000(最低)
l 相同优先级下,接收的顺序要看在清单文件中声明的顺序,先声明的接收者比后声明的要先收到广播
l 有序广播使用sendOrderedBroadcast 方法来发送,使用abortBroadcast 方法拦截
l 广播接收者的优先级在清单文件中声明接收者时,在<intent-filter> 标签下通过设置”android:property”属性来设置
案例-各级人民政府
需求:
在一个应用里面发送自定义的有序广播(模拟主席讲话:每人十斤蘑菇),在另一个应用里面有多个广播接收者负责接收广播,分别是省,市,乡,农民接收者,他们的优先级依次递减,并且蘑菇依次扣减。
代码实现:
创建一个应用程序,在MainActivity里面通过按钮点击发送一个自定义广播:
/**
* 主席讲话: 每人10斤蘑菇
* @param view
*/
publicvoid sendMashroom(View view){
Intent intent = new Intent();
intent.setAction("com.itheima.gov.SENDMASHROOM");
/**
* 参数1 Intent 类型:意图
* 参数2 String 类型receiverPermission,接收器需要的权限
* 参数3 BroadcastReceiver 类型,自己定义的接收者作为最终接收者
* 参数4 Handler 类型,用于执行接收器的回调,如果为null 则在主线程中执行
* 参数5 int 类型,结果代码的初始码
* 参数6 初始化参数
* 参数7Bundle 类型,额外的数据
*/
sendOrderedBroadcast(intent,null, new MyReceiver(), null, 1, "主席讲话: 每人10斤蘑菇", null);
}
注意:该广播是直接被new出来作为一个参数传递给sendOrderedBroadcast该方法的,所以不需要在清单文件进行配置。
创建第二个应用程序,分别定义出省,市,乡,农民广播接收者:
省级政府:
/**
* 省级政府
*/
publicclass ShengReceiver extends BroadcastReceiver {
@Override
publicvoid onReceive(Context context, Intentintent) {
Stringdata = getResultData();
System.out.println("我是省级部门,接受到了中央的指令,"+data);
setResultData("主席讲话:每人5斤蘑菇");
}
}
市级政府:
publicclass ShiReceiver extends BroadcastReceiver {
@Override
publicvoid onReceive(Context context, Intentintent) {
Stringdata = getResultData();
System.out.println("我是市级部门,接受到了中央的指令,"+data);
setResultData("主席讲话:每人3斤蘑菇");
}
}
乡级政府:
publicclass XiangReceiver extends BroadcastReceiver {
@Override
publicvoid onReceive(Context context, Intentintent) {
Stringdata = getResultData();
System.out.println("我是乡级部门,接受到了中央的指令,"+data);
setResultData("主席讲话:每人1斤蘑菇");
}
}
农民:
publicclass NongminReceiver extends BroadcastReceiver {
@Override
publicvoid onReceive(Context context, Intentintent) {
Stringdata = getResultData();
System.out.println("我是农民部门,接受到了中央的指令,"+data);
System.out.println("我是农民部门,思密达,感谢我的太阳 ...");
}
}
对应的在清单文件进行配置:(注意优先级的设置)
<receiver android:name="com.itheima.gov.ShengReceiver">
<intent-filter android:priority="1000">
<action android:name="com.itheima.gov.SENDMASHROOM"/>
</intent-filter>
</receiver>
<receiver android:name="com.itheima.gov.ShiReceiver">
<intent-filter android:priority="800">
<action android:name="com.itheima.gov.SENDMASHROOM"/>
</intent-filter>
</receiver>
<receiver android:name="com.itheima.gov.XiangReceiver">
<intent-filter android:priority="600">
<action android:name="com.itheima.gov.SENDMASHROOM"/>
</intent-filter>
</receiver>
<receiver android:name="com.itheima.gov.NongminReceiver">
<intent-filter android:priority="-1000">
<action android:name="com.itheima.gov.SENDMASHROOM"/>
</intent-filter>
</receiver>
7. 特殊的广播接收者-锁屏与解屏
在Android 中一些操作比较频繁的事件,比如锁屏解屏和电量的变化,也会发送特定的广播。但是此类广播的注册是无法注册在AndroidManifest.xml 中,只能在代码中进行注册。
BroadCastReceiver 的注册方式有两种:1、静态注册(就是通过AndroidManifest.xml 注册)2、动态注册(就是通过代码注册)。在本文中前面使用到的BroadCastReceiver 全部都使用的是静态注册方式,其实也可以使用动态注册,但是对于锁屏解屏和电量变化的监听只能通过动态注册。
接下来做一个锁屏和解屏监听的案例。其实很简单,因此只给出核心部分。
MainActivity.java
publicclass MainActivity extends Activity {
ScreenStatusReceiverreceiver;
@Override
protectedvoid onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
receiver = new ScreenStatusReceiver();
IntentFilterfilter = new IntentFilter();
//添加屏幕关闭的action
filter.addAction("android.intent.action.SCREEN_OFF");
//添加屏幕点亮的action
filter.addAction("android.intent.action.SCREEN_ON");
//注册广播接受者
registerReceiver(receiver, filter);
}
@Override
protectedvoid onDestroy() {
// 取消注册广播接受者
unregisterReceiver(receiver);
receiver = null;
super.onDestroy();
}
}
自定义类继承BroadCastReceiver类
publicclass ScreenStatusReceiver extends BroadcastReceiver {
@Override
publicvoid onReceive(Context context, Intentintent) {
if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {
Log.d("tag", "监听到屏幕点亮了");
}elseif (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
Log.d("tag", "监听到屏幕关闭了");
}
}
}
监听锁屏和解屏不需要额外的权限,因此可以直击运行。
注意:如果我们的应用退出了,那么就无法监听到该广播,可能有人说是因为退出的时候调用了MainActivity 的onDestory()方法,而onDestory()方法中调用了unregisterReceiver(BroadCastReceiver)方法,确实如此的。那么如果我们将onDestory()方法中的unregisterReceiver()方法给去掉会是什么样的情况呢?那我们就一起试一下吧!
将MainActivity.java 中的unregisterReceiver()去掉之后重新运行上面的项目,然后点击back 键,发现logcat 输入了如下错误信息:
上面的信息说我们的MainActivity导致了IntentReceiver 的泄露。ScreenReceiver 在MainActivity 中被注册了但是当MainActivity 销毁的时候没有被反注册。
可见要想让我们的代码更加完善,必须在onDestory 中或者其他地方(必须保证Activity 销毁前)将接收者给反注册掉。
音乐播放器