Android中的广播

2017-05-11  本文已影响548人  sunhaiyu

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"表示启用这个广播,默认为trueandroid: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();
    }
}

这样,这个广播接收器只能接受来自本应用的广播,而且其他程序也不会接收到这个广播了。

自定义带权限的广播

详见WJ_S的CSDN

谁可以接收我发出的广播?

假设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

上一篇下一篇

猜你喜欢

热点阅读