Android中的广播
广播接受器,可以比喻成收音机。而广播则可以看成电台。
Android系统内部相当于已经有一个电台 定义了好多的广播事件,比如外拨电话 短信到来 sd卡状态 电池电量变化...
广播的两种注册方式
静态注册
静态注册写在AndroidManifest.xml
,即使没有进入应用(没有写在onCreate里面),也可以接收到。如接收开机广播。
监听外拨电话
写个拨打电话,自动加区号的功能。区号可以自定义,每次填了区号后,会存到偏好文件中。若不改区号,每次默认打电话都会加这个区号。(这个程序实在没什么意义)
可直接在Android Studio中File -> New -> Other -> Broadcast Receiver。需要重写OnReceive方法,这个方法在广播接受器接收到广播的时候被调用。
AndroidManifest.xml
下注册
<receiver
android:name=".TelReceiver"
android:enabled="true"
android:exported="true">
<intent-filter >
<action android:name="android.intent.action.NEW_OUTGOING_CALL"/>
</intent-filter>
</receiver>
其中android:enabled="true"
表示启用这个广播,默认为true。android:enabled="true"
表示这个广播接收器可以接收本程序以外的广播。其默认值是由receiver中有无intent-filter决定的,如果有intent-filter,默认值为true,否则为false。一般就上面默认生成的写法就好了,显式指定为true更直观不是吗。
同时申请权限<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
,记住一定要申请运行时权限啊,我傻傻的调试了半天......
主界面一个输入框用来自定义默认区号,和一个按钮保存到偏好文件。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.telbroadcastreceivertest.MainActivity">
<EditText
android:id="@+id/et_area_code"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/bt_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="保存区号"/>
</LinearLayout>
MainActivity
package com.example.telbroadcastreceivertest;
import android.Manifest;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private EditText editText;
private Context mContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContext = this;
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.PROCESS_OUTGOING_CALLS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.PROCESS_OUTGOING_CALLS}, 1);
}
editText = (EditText) findViewById(R.id.et_area_code);
Button button = (Button) findViewById(R.id.bt_save);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String areaCode = editText.getText().toString().trim();
SharedPreferences spf = getSharedPreferences("code", MODE_PRIVATE);
// 保存区号到偏好文件,下次拨打电话默认加这个前缀
spf.edit().putString("code", areaCode).apply();
Toast.makeText(mContext, "区号保存成功", Toast.LENGTH_SHORT).show();
}
});
}
}
广播接收器,可以拦截外拨电话的有序广播
package com.example.telbroadcastreceivertest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
public class TelReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
SharedPreferences spf = context.getSharedPreferences("code", Context.MODE_PRIVATE);
String areaCode = spf.getString("code", "");
// 接收前一个广播接收者的所设定的数据,这里是拨号器的广播接收者,数据是电话号码
String phoneNumber = getResultData();
// 区号一般以0开头,若号码不以0开头,就加上区号
if (!phoneNumber.startsWith("0")) {
setResultData(areaCode + phoneNumber);
}
}
}
向外拨打电话时系统会发出一个有序广播,android.intent.action.NEW_OUTGOING_CALL
,虽然该广播最终会被拔号器里的广播接收者所接收并实现电话拔打,但我们可以在广播传递给拔号广播接收者之前先得到该广播,并添加了区号后再拨打出去。
填写默认的区号。
由于使用真机,拨号使用了一个空号。
看图,确实是添加了区号再拨打出去的
测试成功后,赶紧给卸载了,这会影响电话拨打功能的。
动态注册
监听SD卡的挂载/未挂载
注意:这里所说的SD卡是真实的外置SD卡。
之前使用真机,还有模拟器,SD卡的选择是internal storage而非protable storage。这样就不带外置SD卡,所以一直接收不到广播,折腾了半天。引以为戒!
AndroidManifest.xml里面
<receiver android:name=".SDReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_MOUNTED"/>
<action android:name="android.intent.action.MEDIA_REMOVED"/>
<action android:name="android.intent.action.MEDIA_UNMOUNTED"/>
<!-- 一定要指定下面这个scheme为"file",否则无法接收到广播 -->
<data android:scheme="file"/>
</intent-filter>
</receiver>
上面的写法是静态注册,用动态注册的方式如下,也一定要加上scheme。注意动态注册只能当前Activity能接收到广播,因为是写在onCreate方法里的嘛。
// onCreate方法内
IntentFilter intentFilter = new IntentFilter();
// 同时加了三个action,可以接收到这三种系统广播
intentFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
intentFilter.addAction(Intent.ACTION_MEDIA_EJECT);
// scheme必须加
intentFilter.addDataScheme("file");
// 实例化广播接收者
SDReceiver sdReceiver = new SDReceiver();
// 注册广播接收器,接收一个实例和intentFilter。匹配action和data
registerReceiver(sdReceiver , intentFilter);
// !!! 注册了还必须注销,在onDestroy方法内
@Override
protected void onDestroy() {
unregisterReceiver(telReceiver);
super.onDestroy();
}
动态注册,必须注销。而静态注册,无需操心注销的事。
动态注册的优先级是要高于静态注册优先级的
然后新建一个Broadcast Receiver重写receive方法,判断状态
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if ("android.intent.action.MEDIA_MOUNTED".equals(action)) {
Log.d("SD卡", "外置SD卡存在,且已挂载mounted");
} else if ("android.intent.action.MEDIA_UNMOUNTED".equals(action)) {
Log.d("SD卡", "外置SD卡存在,但未挂载unmounted");
} else if (Intent.ACTION_MEDIA_EJECT.equals(action)) {
Log.d("SD卡", "外置SD卡卸载ejected");
} else if (Intent.ACTION_MEDIA_REMOVED.equals(action)) {
Log.d("SD卡", "外置SD卡移除removed");
}
当挂载了外置SD卡时候,就会打印外置SD卡存在,且已挂载mounted
,正常卸载时候打印外置SD卡存在,但未挂载
和外置SD卡卸载ejected
。当拔出了SD卡的情况,不仅打印上面两个,还会打印外置SD卡移除removed
。
拓展
现在的手机一般很少插入了真实的外置SD卡,我们之前所说的SD卡指的是虚拟的内置SD卡,又叫做internal storage,路径是/mnt/sdcard
或者/storage/emulated/0
又或者/sdcard
,这三个路径指向同一个地方。
Environment.getExternalStorageDirectory().getPath(); // 这里小米真机打印/storage/emulated/0
。从Androidkk4.4开始,第三方应用程序是无法访问(读/写)外置SD卡的;仅仅只有系统级别的并且使用系统签名的APP可以访问外置SD卡。但就我测试环境(API25)来看,是并不影响监听外置SD卡状态的。
还可以判断内置SD卡的状态,使用如下方法。当然外置SD的拔插,都不影响内置SD卡。都会打印mounted
Log.d("SDcard", Environment.getExternalStorageState()); // 打印mounted
Log.d("SDcard", Environment.getExternalStorageDirectory().getPath()); // 打印 /storage/emulated/0
接收(拦截)短信
注册
<!-- 申请接收短信的权限 -->
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<!-- 省略部分代码 -->
<receiver
android:name=".SmsReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
申请的权限记得要动态申请。
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECEIVE_SMS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.RECEIVE_SMS}, 1);
}
广播接收器
package com.example.telbroadcastreceivertest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.telephony.SmsMessage;
import android.util.Log;
public class SmsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 每一个object为一条短信,实际返回的是byte[][],一个pdu对应byte[]
Object[] smss = (Object[]) intent.getExtras().get("pdus");
Log.d("sms", String.valueOf(smss.length));
for (Object sms : smss) {
// API23后,必须加入format参数
String format = intent.getStringExtra("format"); // 模拟器,打印3gpp
Log.d("sms", format);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) sms, format);
// 获取发送短信的内容
// 功能更强大,如果有,可以得到email message body和address
String dispalyBody = smsMessage.getDisplayMessageBody();
String displayAddr = smsMessage.getDisplayOriginatingAddress();
// 纯文本的消息体
String body = smsMessage.getMessageBody();
// 获取发送者号码
String address = smsMessage.getOriginatingAddress();
Log.d("sms", "disBody:"+dispalyBody); // 由于发送的就是一般的文本短信,打印内容和下面一样
Log.d("sms", "body:"+body);
Log.d("sms", "disAddr:"+displayAddr); // 打印内容和下面一样
Log.d("sms", "addr:"+address);
}
}
}
}
API23开始,SmsMessage.createFromPdu(byte[] sms);
被弃用。使用SmsMessage.createFromPdu((byte[]) sms, format);
format可以用String format = intent.getStringExtra("format");
获取。
这个例子在真机上(可恶的小米5S)测试又失败了。估计有啥流氓软件给先拦截了。不过,在模拟器上测试成功。
监听应用的安装、更新、卸载
注册
<receiver android:name=".APPReceiver">
<intent-filter>
<!-- 安装应用 -->
<action android:name="android.intent.action.PACKAGE_ADDED" />
<!-- 更新已有应用 -->
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<!-- 卸载应用 -->
<action android:name="android.intent.action.PACKAGE_REMOVED" />
<!-- 携带包名,这个必须要加上 -->
<data android:scheme="package" />
</intent-filter>
</receiver>
不需要申请啥权限
广播接收器
package com.example.telbroadcastreceivertest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
public class APPReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 判断广播类型
String action = intent.getAction();
//获取包名
// intent.getData().toString
String appName = intent.getDataString();
if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
Log.i("APPReceiver", "ADD" + appName);
} else if (Intent.ACTION_PACKAGE_REPLACED.equals(action)) {
Log.i("APPReceiver", "REPLACED" + appName);
} else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
Log.i("APPReceiver", "REMOVE" + appName);
}
}
}
应用在更新时,会先REMOVED,然后在REPLACED。在本例中,自己本身被安装和卸载,接收不到ADDED和REMOVED广播。
应用未启动过,接收不到广播
资料来自张明云的知识共享
从Android3.1开始,新安装的程序会被置于”stopped”状态,并且只有在至少手动启动这个程序一次后该程序才会改变状态,能够正常接收到指定的广播消息。Android这样做的目的是防止广播无意或者不必要地开启未启动的APP后台服务。也就是说在Android3.1及以上的版本,在未启动的情况下通过应用自身完成一些操作是不可能的,但Android提供了一种借助其它应用发送指定Flag广播的方式,达到应用在未启动的情况下仍然能够收到消息的效果。
从Android 3.1开始,系统给Intent定义了两个新的Flag,分别为FLAG_INCLUDE_STOPPED_PACKAGES(表示包含未启动的App)和FLAG_EXCLUDE_STOPPED_PACKAGES(表示不包含未启动的App),用来控制Intent是否要对处于停止状态的App起作用,具体的操作方式如下
Intent intent = new Intent(); intent.setAction("com.xxx.xxx.ACTION_XXXX"); // 这句是关键 intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); sendBroadcast(intent);
监听开机广播
注册
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
申请权限
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
广播接收器
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Boot Complete", Toast.LENGTH_SHORT).show();
}
一些特殊的广播
比如操作特别频繁的广播事件,屏幕的关闭和打开 ,电量的变化等广播接收器静态注册无效
android.intent.action.SCREEN_ON
android.intent.action.SCREEN_OFF
android.intent.action.BATTERY_CHANGED
android.intent.action.CONFIGURATION_CHANGED
android.intent.action.TIME_TICK
因为这些事android的基本事件,如果大多数程序都监听,会大大的拖慢整个系统(占用内存等),所以android不鼓励我们在程序退出的情况下监听这些事件。不过还是可以通过在service里面注册广播接受器...来解决。
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
intentFilter.addAction(Intent.ACTION_SCREEN_ON);
screenReceiver = new ScreenReceiver();
registerReceiver(screenReceiver, intentFilter);
别忘了注销
@Override
protected void onDestroy() {
unregisterReceiver(screenReceiver);
super.onDestroy();
}
监听网络状态变化
可以静态注册,但是已经被弃用。最好动态注册。
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
networkReceiver = new NetworkReceiver();
registerReceiver(networkReceiver, intentFilter);
取消注册
@Override
protected void onDestroy() {
unregisterReceiver(screenReceiver);
super.onDestroy();
}
@Override
public void onReceive(Context context, Intent intent) {
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
// 获得正在使用的网络,若无网络连接返回null,所以需要判断
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isAvailable()) {
String typeName = networkInfo.getTypeName(); // 返回MOBILE或WIFI
Toast.makeText(context, typeName + " network is available", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(context, "network is unavailable", Toast.LENGTH_SHORT).show();
}
}
记得申请权限 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
自定义广播
标准广播和有序广播
- 标准广播:异步执行,广播发出后,所有广播接收器几乎同时接收到这条广播。无顺序可言,效率高但是无法被截获,也无法修改数据。可想象成开会时,听领导讲话。
- 有序广播:同步执行,广播发出后,同一时刻只有一个广播接收器能收到。当这个广播接收器处理完成之后,可修改数据后传给下一个接收器,也可以选择截获(不继续往下传递),显然这是有顺序的。可想象成一个人给另外一个人传话,我可以不告诉你呀。
发送标准广播
定义了两个广播接受器,接收同一个自定义广播com.example.broadcasttest.MY_BROADCAST
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>
</receiver>
<receiver
android:name=".AnotherReceiver"
android:enabled="true"
android:exported="true" >
<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>
</receiver>
然后在某个活动中,匹配了action在发送广播就行了。
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
sendBroadcast(intent);
两个广播接收器
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Received in MyBroadcastReceiver", Toast.LENGTH_SHORT).show();
}
}
// 另外一个文件
public class AnotherReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Received in AnotherBroadcastReceiver", Toast.LENGTH_SHORT).show();
}
}
上面的两个接收器都会接收到这个广播。在无序广播中,abortBroadcast()是无效的,而且会在logcat发出警告。
发送有序广播
还是上面的两个接收器,不同的是给AnotherReceiver设置了优先级。priority可在[-1000,1000]之间设置,默认为0,值越大优先级越高。
<receiver
android:name=".AnotherReceiver"
android:enabled="true"
android:exported="true" >
<intent-filter android:priority="1000">
<action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>
</receiver>
且在AnotherReceiver中阻断了传播
public class AnotherReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Received in AnotherBroadcastReceiver", Toast.LENGTH_SHORT).show();
// 截获后阻断传播
abortBroadcast();
}
}
这样只有AnotherReceiver能接收打广播了,由于在此阻断了传播,所以 MyBroadcastReceiver接收不到。
不过如果这样发送MyBroadcastReceiver就又可以接收到了
sendOrderedBroadcast(intent, null, new MyBroadcastReceiver(), null, RESULT_OK, null, null);
注意第三个参数,是最终接收器,不管有没有被截获,最后都会传到这个接收器。所以,上面的截获对于MyBroadcastReceiver是无意义的,因为在发送有序广播时指定了这个接收器为最终接收器。而且最终的Receiver无需注册。把上面的MyBroadcastReceiver注册代码删掉,也能接收到。
本地广播
前面发送和接收的都是全局广播,发出的广播可以被任何应用程序接收到;而且也可以接收来自其他任何应用程序的广播。这样容易引起安全问题。如发送一些敏感数据,如果被其他软件截获,又或者其他程序不停向我们发送广播。
使用本地广播,使得广播只能在应用程序内部传递。广播接收器也只能接收来自本应用程序的广播。这样安全性就得到保障。
本地广播无法通过静态注册来接收。因为静态就是让程序在未启动的情况下也能收到广播。但是在发送本地广播的时候,我们的程序肯定是在运行的。因此完全不需要使用静态注册.
需要使用到LocalBroadcastManager
mContext = this;
// 先注册
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("com.example.action.LOCAL_BROADCAST");
localReceiver = new LocalReceiver();
// 获得实例
localBroadcastManager = LocalBroadcastManager.getInstance(mContext);
// 使用localBroadcastManager的注册方法
localBroadcastManager.registerReceiver(localReceiver, intentFilter);
// 再发送广播
Intent intent = new Intent("com.example.action.LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(intent);
// 省略了部分代码
@Override
protected void onDestroy() {
// 取消注册也要是要localBroadcastManager的方法
localBroadcastManager.unregisterReceiver(localReceiver);
super.onDestroy();
}
LocalReceiver
package com.example.administrator.broadcasttest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class LocalReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "LocalReceiver", Toast.LENGTH_SHORT).show();
}
}
这样,这个广播接收器只能接受来自本应用的广播,而且其他程序也不会接收到这个广播了。
自定义带权限的广播
谁可以接收我发出的广播?
假设appA发出了带有权限的广播,那么appB中的广播接收器想要接收到,必须申请appA定义的权限。
appA中自定义权限
<permission
android:name="com.example.broadcasttest.RECEICVE_ABC"
android:protectionLevel="normal"/>
其中protectionLevel有如下几种比较常见
normal:默认的,普通权限。应用安装前,用户可以看到相应的权限,但无需用户主动授权。
dangerous:危险权限,需要动态申请。Android会弹出对话框要求用户进行授权。常见的如:网络使用权限,发送短信权限、联系人信息使用权限等。
signature:只有和该apk(定义了这个权限的apk)用相同的签名的应用才可以申请该权限。
appA发送带有权限的广播
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
sendBroadcast(intent, "com.example.broadcasttest.RECEICVE_ABC");
appB中的广播接收器若想接收这个广播,必须申请权限<uses-permission android:name="com.example.broadcasttest.RECEICVE_ABC"/>
,没有申请权限的一概收不到。
谁有权发送广播给我?
比如appB想发送广播给appA,而appA带有权限。
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
sendBroadcast(intent);
appA中自定义的权限
<permission
android:name="com.example.broadcasttest.SEND_ABC"
android:protectionLevel="normal"/>
且appA的接收器加上权限
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true"
android:permission="com.example.broadcasttest.SEND_ABC" >
<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>
</receiver>
这样appB发送的广播,想让appA也收到。则必须申请appA定义的权限。appB中
<uses-permission android:name="com.example.broadcasttest.SEND_ABC" />
注意:onReceive方法不要进行耗时操作,当该方法运行时间过长还没结束,程序就会报错。
by @sunhaiyu
2017.5.11
网友评论