广播机制简介
Android 中的每个应用程序都可以对自己感兴趣的广播进行注册,这样该程序就只会收到自己所关心的广播内容,这些广播可能是来自于系统的,也可能是来自于其他应用程序的。
Android 提供了一套完整的API,允许应用程序自由地发送和接收广播。
发送广播的方法其实就是借助Intent 。
而接收广播的方法则需要引入一个新的概念—— BroadcastReceiver
先来了解一下广播的类型。
Android 中的广播主要可以分为两种类型: 标准广播和有序广播。
标准广播(normalbroadcasts )
是一种完全异步执行的广播,在广播发出之后,所有的
BroadcastReceiver 几乎会在同一时刻收到这条广播消息,
因此它们之间没有任何先后顺序可言。
这种广播的效率会比较高,但同时也意味着它是无法被截断的。
标准广播的工作流程如图所示。
有序广播(orderedbroadcasts )
则是一种同步执行的广播,在广播发出之后,同一时刻只会有一个BroadcastReceiver 能够收到这条广播消息,当这个BroadcastReceiver 中的逻辑执行完毕后,广播才会继续传递。
所以此时的BroadcastReceiver 是有先后顺序的,
优先级高的BroadcastReceiver 就可以先收到广播消息,并且前面的BroadcastReceiver还可以截断正在传递的广播,这样后面的BroadcastReceiver 就无法收到广播消息了。
有序广播的工作流程如图所示。
接收系统广播
Android 内置了很多系统级别的广播,可以在应用程序中通过监听这些广播来得到各种系统的状态信息。
比如手机开机完成后会发出一条广播,
电池的电量发生变化会发出一条广播,
系统时间发生改变也会发出一条广播,等等。
如果想要接收这些广播,就需要使用BroadcastReceiver ,下面我们就来看一下它的具体用法
动态注册监听时间变化
可以根据自己感兴趣的广播,自由地注册BroadcastReceiver ,这样当有相应的广播发出时, 相应的BroadcastReceiver 就能够收到该广播,并可以在内部进行逻辑处理。
注册BroadcastReceiver 的方式一般有两种:
在代码中注册和在AndroidManifest.xml 中注册。
其中前者也被称为动态注册,后者也被称为静态注册。
那么如何创建一个BroadcastReceiver 呢?
其实只需新建一个类,让它继承自BroadcastReceiver ,并重写父类的onReceive()方法就行了。
这样当有广播到来时,onReceive()方法就会得到执行,具体的逻辑就可以在这个方法中处理。
下面就先通过动态注册的方式编写一个能够监听时间变化的程序,借此学习一下
BroadcastReceiver 的基本用法。
新建一个TimeReceiverActivity ,如下所示:
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.androidlearn.R
class TimeReceiverActivity : AppCompatActivity() {
lateinit var timeChangeReceiver: TimeChangeReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_time_receiver)
val intentFilter = IntentFilter()
intentFilter.addAction("android.intent.action.TIME_TICK")
timeChangeReceiver = TimeChangeReceiver()
registerReceiver(timeChangeReceiver, intentFilter)
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(timeChangeReceiver)
}
inner class TimeChangeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Toast.makeText(context, "Time has changed", Toast.LENGTH_SHORT).show()
}
}
}
可以看到,我们定义了一个内部类TimeChangeReceiver,这个类是继承自BroadcastReceiver的,并重写了父类的onReceive()方法。
这样每当系统时间发生变化时,onReceive()方法就会得到执行,这里只是简单地使用Toast 提示了一段文本信息。
然后观察onCreate()方法,首先我们创建了一个IntentFilter的实例,并给它添加了一个
值为android.intent.action.TIME_TICK的action ,为什么要添加这个值呢?
因为当系统时间发生变化时,系统发出的正是一条值为android.intent.action.TIME_TICK的广播,
也就是说我们的BroadcastReceiver想要监听什么广播,就在这里添加相应的action 。
接下来创建了一个TimeChangeReceiver的实例,然后调用registerReceiver()方法进行注册,将TimeChangeReceiver的实例和IntentFilter的实例都传了进去,
这样TimeChangeReceiver就会收到所有值为android.intent.action.TIME_TICK的广播,
也就实现了监听系统时间变化的功能。
最后要记得,动态注册的BroadcastReceiver一定要取消注册才行,这里我们是在onDestroy()方法中通过调用unregisterReceiver()方法来实现的。
现在运行一下程序,然后静静等待时间发生变化。系统每隔一分钟就会发出一条android.intent.action.TIME_TICK的广播,因此我们最多只需要等待一分钟就可以收到这条广播了。
这就是动态注册BroadcastReceiver的基本用法,虽然这里我们只使用了一种系统广播来举例,但是接收其他系统广播的用法是一模一样的。
Android 系统还会在亮屏熄屏、电量变化、网络变化等场景下发出广播。
如果你想查看完整的系统广播列表,可以到如下的路径中去查看:
<Android SDK>/platforms/<任意android api版本>/data/broadcast_actions.txt
静态注册实现开机启动
动态注册的BroadcastReceiver 可以自由地控制注册与注销,在灵活性方面有很大的优势。
但是它存在着一个缺点,即必须在程序启动之后才能接收广播,因为注册的逻辑是写在onCreate()方法中的。
那么有没有什么办法可以让程序在未启动的情况下也能接收广播呢?
这就需要使用静态注册的方式了。
其实从理论上来说,动态注册能监听到的系统广播,静态注册也应该能监听到,在过去的
Android 系统中确实是这样的。
但是由于大量恶意的应用程序利用这个机制在程序未启动的情况下监听系统广播,从而使任何应用都可以频繁地从后台被唤醒,严重影响了用户手机的电量和性能,因此Android 系统几乎每个版本都在削减静态注册BroadcastReceiver 的功能。
在Android 8.0 系统之后,所有隐式广播都不允许使用静态注册的方式来接收了。
隐式广播指的是那些没有具体指定发送给哪个应用程序的广播,大多数系统广播属于隐式广播,
但是少数特殊的系统广播目前仍然允许使用静态注册的方式来接收。这些特殊的系统广播列表详见
developer.android.google.cn/guide/compo… 。
在这些特殊的系统广播当中,有一条值为android.intent.action.BOOT_COMPLETED的广播,这是一条开机广播。
这里我们准备实现一个开机启动的功能。在开机的时候,我们的应用程序肯定是没有启动的,
因此这个功能显然不能使用动态注册的方式来实现,而应该使用静态注册的方式来接收开机广播,然后在onReceive()方法里执行相应的逻辑,这样就可以实现开机启动的功能了。
上面我们使用内部类的方式创建的BroadcastReceiver ,其实还
可以通过AndroidStudio 提供的快捷方式来创建。右击com.example.broadcasttest 包
→New→Other→BroadcastReceiver ,会弹出如图的窗口。
可以看到,这里我们将创建的类命名为BootCompleteReceiver , Exported属性表示是否允许这个BroadcastReceiver 接收本程序以外的广播,
Enabled属性表示是否启用这个BroadcastReceiver 。
勾选这两个属性,点击“Finish” 完成创建。
然后修改BootCompleteReceiver 中的代码,如下所示:
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
class BootCompleteReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// This method is called when the BroadcastReceiver is receiving an Intent broadcast.
Toast.makeText(context, "Boot Complete", Toast.LENGTH_LONG).show()
}
}
代码非常简单,只是在onReceive()方法中使用Toast 弹出一段提示信息。
另外,静态的BroadcastReceiver 一定要在AndroidManifest.xml 文件中注册才可以使用。
不过,由于我们是使用AndroidStudio 的快捷方式创建的BroadcastReceiver ,因此注册这一步已经自动完成了。
打开AndroidManifest.xml 文件瞧一瞧,代码如下所示:
可以看到,<application>标签内出现了一个新的标签<receiver>,
所有静态的BroadcastReceiver 都是在这里进行注册的。
它的用法其实和<activity>标签非常相似,也是通过android:name指定具体注册哪一个BroadcastReceiver ,
而enabled和exported属性则是根据我们刚才勾选的状态自动生成的。
不过目前的BootCompleteReceiver 是无法收到开机广播的,因为我们还需要对AndroidManifest.xml 文件进行修改才行,如下所示:
由于Android 系统启动完成后会发出一条值为android.intent.action.BOOT_COMPLETED
的广播,因此我们在<receiver>标签中又添加了一个<intent-filter>标签,并在里面声
明了相应的action 。
另外,Android 系统为了保护用户设备的安全和隐私,做了严格的规定: 如果程序需要进行一些对用户来说比较敏感的操作,必须在AndroidManifest.xml 文件中进行权限声明,否则程序将会直接崩溃。
比如这里接收系统的开机广播就是需要进行权限声明的,所以我们在上述代码中使用<uses-permission>标签声明
了android.permission.RECEIVE_BOOT_COMPLETED权限。
Android6.0 系统中引入了更加严格的运行时权限,从而能够更好地保证用户设备的安全和隐私。
重新运行程序,现在我们的程序已经可以接收开机广播了。长按模拟器右侧工具栏中的Power
按钮,会在模拟器界面上弹出关机重启选项,如图所示
点击“Restart” 按钮重启模拟器,在启动完成之后就会收到开机广播,如图所示。
到目前为止,我们在BroadcastReceiver 的onReceive()方法中只是简单地使用Toast 提示了一段文本信息,
当你真正在项目中使用它的时候,可以在里面编写自己的逻辑。
需要注意的是,不要在onReceive()方法中添加过多的逻辑或者进行任何的耗时操作,
因为BroadcastReceiver 中是不允许开启线程的,当onReceive()方法运行了较长时间而没有结束时,程序就会出现错误。
发送自定义广播
接下来学习一下如何在应用程序中发送自定义的广播。
前面已经介绍过了,广播主要分为两种类型:标准广播和有序广播。
这里通过实践的方式来看一下这两种广播具体的区别。
发送标准广播
在发送广播之前,我们还是需要先定义一个BroadcastReceiver 来准备接收此广播,不然发出去也是白发。
因此新建一个MyBroadcastReceiver ,并在onReceive()方法中加入如下代码:
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
class MyBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Toast.makeText(context, "received in MyBroadcastReceiver",
Toast.LENGTH_SHORT).show()
}
}
当MyBroadcastReceiver 收到自定义的广播时,就会弹出“received in MyBroadcastR eceiver ”的提示。
然后在AndroidManifest.xml 中对这个BroadcastReceiver 进行修改
可以看到,这里让MyBroadcastReceiver 接收一条值为
com.example.broadcasttest.MY_BROADCAST的广播,因此待会儿在发送广播的时候,我们就需要发出这样的一条广播。
接下来修改activity_main.xml 中的代码,如下所示
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send Broadcast"
/>
</LinearLayout>
这里在布局文件中定义了一个按钮,用于作为发送广播的触发点。然后修改MainActivity 中的代码,如下所示:
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val intent = Intent("com.example.broadcasttest.MY_BROADCAST")
intent.setPackage(packageName)
sendBroadcast(intent)
}
...
}
...
}
可以看到,我们在按钮的点击事件里面加入了发送自定义广播的逻辑。
首先构建了一个Intent 对象,并把要发送的广播的值传入。
然后调用Intent的setPackage()方法,并传入当前应用程序的包名。
packageName是getPackageName()的语法糖写法,用于获取当前应用程序的包名。
最后调用sendBroadcast()方法将广播发送出去,这样所有监听com.example.broadcasttest.MY_BROADCAST这条广播的BroadcastReceiver 就会收到消息了。
此时发出去的广播就是一条标准广播。
这里我还得对第2步调用的setPackage()方法进行更详细的说明。
前面已经说过,在Android8.0 系统之后,静态注册的BroadcastReceiver 是无法接收隐式广播的,而默认情况下我们发出的自定义广播恰恰都是隐式广播。
因此这里一定要调用setPackage()方法,指定这条广播是发送给哪个应用程序的,从而让它变成一条显式广播,否则静态注册的BroadcastReceiver 将无法接收到这条广播。
现在重新运行程序,并点击“Send Broadcast” 按钮,效果如图所示。
这样我们就成功完成了发送自定义广播的功能。
另外,由于广播是使用Intent 来发送的,因此你还可以在Intent 中携带一些数据传递给相应的
BroadcastReceiver ,这一点和Activity 的用法是比较相似的。
发送有序广播
和标准广播不同,有序广播是一种同步执行的广播,并且是可以被截断的。
为了验证这一点,我们需要再创建一个新的BroadcastReceiver 。
新建AnotherBroadcastReceiver ,代码如下所示:
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
class AnotherBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Toast.makeText(
context, "received in AnotherBroadcastReceiver",
Toast.LENGTH_SHORT
).show()
}
}
很简单,这里仍然是在onReceive()方法中弹出了一段文本信息。
然后在AndroidManifest.xml 中对这个BroadcastReceiver 的配置进行修改,代码如下所示
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest">
...
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<receiver
android:name=".AnotherBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>
</receiver>
</application>
</manifest>
可以看到,AnotherBroadcastReceiver 同样接收的是com.example.broadcasttest.MY_BROADCAST这条广播。
现在重新运行程序,并点击“Send Broadcast” 按钮,就会分别弹出两次提示信息,如图所示。
不过,到目前为止,程序发出的都是标准广播,现在我们来尝试一下发送有序广播。
重新回到BroadcastTest 项目,然后修改MainActivity 中的代码,如下所示:
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val intent = Intent("com.example.broadcasttest.MY_BROADCAST")
intent.setPackage(packageName)
sendOrderedBroadcast(intent, null)
}
...
}
...
}
可以看到,发送有序广播只需要改动一行代码,即将sendBroadcast()方法改成sendOrderedBroadcast()方法。
sendOrderedBroadcast()方法接收两个参数: 第一个参数仍然是Intent;第二个参数是一个与权限相关的字符串,这里传入null就行了。
现在重新运行程序,并点击“Send Broadcast” 按钮,你会发现,两个BroadcastReceiver 仍然都可以收到这条广播。
看上去好像和标准广播并没有什么区别嘛。不过别忘了,这个时候的BroadcastReceiver 是有
先后顺序的,而且前面的BroadcastReceiver 还可以将广播截断,以阻止其继续传播
那么该如何设定BroadcastReceiver 的先后顺序呢?当然是在注册的时候进行设定了,修改
AndroidManifest.xml 中的代码,如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest">
...
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter android:priority="100">
<action android:name="com.example.broadcasttest.MY_BROADCAST"/>
</intent-filter>
</receiver>
...
</application>
</manifest>
可以看到,我们通过android:priority属性给BroadcastReceiver 设置了优先级,优先级比较高的BroadcastReceiver 就可以先收到广播。
这里将MyBroadcastR eceiver 的优先级设成了100 ,以保证它一定会在AnotherBroadcastReceiver 之前收到广播。
既然已经获得了接收广播的优先权,那么MyBroadcastReceiver 就可以选择是否允许广播继续传递了。修改MyBroadcastReceiver 中的代码,如下所示:
class MyBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Toast.makeText(context, "received in MyBroadcastReceiver",
Toast.LENGTH_SHORT).show()
abortBroadcast()
}
}
如果在onReceive()方法中调用了abortBroadcast()方法,就表示将这条广播截断,后面
的BroadcastReceiver 将无法再接收到这条广播。
现在重新运行程序,并点击“Send Broadcast” 按钮,你会发现只有MyBroadcastReceiver 中的Toast 信息能够弹出,说明这条广播经过MyBroadcastReceiver 之后确实终止传递了。
广播的最佳实践:实现强制下线功能
强制下线应该算是一个比较常见的功能,比如如果你的QQ号在别处登录了,就会将你强制挤下线。
其实实现强制下线功能的思路比较简单,只需要在界面上弹出一个对话框,让用户无法进行任何其他操作,必须点击对话框中的“确定”按钮,然后回到登录界面即可。
可是这样就会存在一个问题:
当用户被通知需要强制下线时,可能正处于任何一个界面,难道要在每个界面上都编写一个弹出对话框的逻辑?
如果你真的这么想,那思路就偏远了。我们完全可以借助广播知识,非常轻松地实现这一功能。
新建一个BroadcastBestPractice 项目,然后开始动手吧。
强制下线功能需要先关闭所有的Activity ,然后回到登录界面。先创建一个ActivityCollector类用于管理所有的Activity ,代码如下所示:
object ActivityCollector {
private val activities = ArrayList<Activity>()
fun addActivity(activity: Activity) {
activities.add(activity)
}
fun removeActivity(activity: Activity) {
activities.remove(activity)
}
fun finishAll() {
for (activity in activities) {
if (!activity.isFinishing) {
activity.finish()
}
}
activities.clear()
}
}
然后创建BaseActivity类作为所有Activity 的父类,代码如下所示
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ActivityCollector.addActivity(this)
}
override fun onDestroy() {
super.onDestroy()
ActivityCollector.removeActivity(this)
}
}
接下来需要创建一个LoginActivity来作为登录界面,并让AndroidStudio 帮我们自动生成相应的布局文件。
然后编辑布局文件activity_login.xml ,代码如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Account:" />
<EditText
android:id="@+id/accountEdit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:layout_width="90dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="18sp"
android:text="Password:" />
<EditText
android:id="@+id/passwordEdit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:inputType="textPassword" />
</LinearLayout>
<Button
android:id="@+id/login"
android:layout_width="200dp"
android:layout_height="60dp"
android:layout_gravity="center_horizontal"
android:text="Login" />
</LinearLayout>
这里我们使用LinearLayout 编写了一个登录布局,最外层是一个纵向的LinearLayout ,里面包含了3行直接子元素。
第一行是一个横向的LinearLayout ,用于输入账号信息;
第二行也是一个横向的LinearLayout ,用于输入密码信息;
第三行是一个登录按钮。
接下来修改LoginActivity 中的代码,如下所示:
import android.content.Intent
import android.os.Bundle
import android.text.Layout
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.androidlearn.MainActivity
import com.example.androidlearn.R
class LoginActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
val login = findViewById<Button>(/* id = */ R.id.login)
login.setOnClickListener {
val accountEdit = findViewById<EditText>(/* id = */ R.id.accountEdit)
val passwordEdit = findViewById<EditText>(/* id = */ R.id.passwordEdit)
val account = accountEdit.text.toString()
val password = passwordEdit.text.toString()
// 如果账号是admin且密码是123456,就认为登录成功
if (account == "admin" && password == "123456") {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
} else {
Toast.makeText(this, "account or password is invalid",
Toast.LENGTH_SHORT).show()
}
}
}
}
这里我们模拟了一个非常简单的登录功能。
首先将LoginActivity 的继承结构改成继承自BaseActivity ,
然后在登录按钮的点击事件里对输入的账号和密码进行判断:
如果账号是admin 并且密码是123456 ,就认为登录成功并跳转到MainActivity ,否则就提示用户账号或密码错误。
因此,你可以将MainActivity 理解成是登录成功后进入的程序主界面,这里我们并不需要在主界面提供什么花哨的功能,只需要加入强制下线功能就可以了。修改activity_main.xml 中的代码,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/forceOffline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send force offline broadcast" />
</LinearLayout>
非常简单,只有一个按钮用于触发强制下线功能。
然后修改MainActivity 中的代码,如下所示:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val forceOffline = findViewById<Button>(R.id.forceOffline)
forceOffline.setOnClickListener {
val intent = Intent("com.example.broadcastbestpractice.FORCE_OFFLINE")
sendBroadcast(intent)
}
}
}
同样非常简单,不过这里有个重点,
我们在按钮的点击事件里发送了一条广播,广播的值为 com.example.broadcastbestpractice.FORCE_OFFLINE,
这条广播就是用于通知程序强制用户下线的。
也就是说,强制用户下线的逻辑并不是写在MainActivity 里的,而是应该写在接收这条广播的BroadcastReceiver 里。
这样强制下线的功能就不会依附于任何界面了,不管是在程序的任何地方,只要发出这样一条广播,就可以完成强制下线的操作了。
那么毫无疑问,接下来我们就需要创建一个BroadcastReceiver 来接收这条强制下线广播。
唯一的问题就是,应该在哪里创建呢?
由于BroadcastReceiver 中需要弹出一个对话框来阻塞用户的正常操作,但如果创建的是一个静态注册的BroadcastR eceiver ,是没有办法在onReceive()方法里弹出对话框这样的UI控件的,
而我们显然也不可能在每个Activity 中都注册一个动态的BroadcastReceiver 。
那么到底应该怎么办呢?答案其实很明显,只需要在BaseActivity 中动态注册一个
BroadcastReceiver 就可以了,
因为所有的Activity 都继承自BaseActivity 。
修改BaseActivity 中的代码,如下所示:
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
open class BaseActivity : AppCompatActivity() {
lateinit var receiver: ForceOfflineReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ActivityCollector.addActivity(this)
}
override fun onResume() {
super.onResume()
val intentFilter = IntentFilter()
intentFilter.addAction("com.example.broadcastbestpractice.FORCE_OFFLINE")
receiver = ForceOfflineReceiver()
registerReceiver(receiver, intentFilter)
}
override fun onPause() {
super.onPause()
unregisterReceiver(receiver)
}
override fun onDestroy() {
super.onDestroy()
ActivityCollector.removeActivity(this)
}
inner class ForceOfflineReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
AlertDialog.Builder(context).apply {
setTitle("Warning")
setMessage("You are forced to be offline. Please try to login again.")
setCancelable(false)
setPositiveButton("OK") { _, _ ->
ActivityCollector.finishAll() // 销毁所有Activity
val i = Intent(context, LoginActivity::class.java)
context.startActivity(i) // 重新启动LoginActivity
}
show()
}
}
}
}
先来看一下 ForceOfflineReceiver 中的代码,这次onReceive()方法里可不再是仅仅弹出一个Toast 了,而是加入了较多的代码.
那我们就来仔细看看吧。首先是使用AlertDialog.Builder
构建一个对话框。注意,这里一定要调用setCancelable()方法将对话框设为不可取消,否则用户按一下Back 键就可以关闭对话框继续使用程序了。
然后使用setPositiveButton()方法给对话框注册确定按钮,当用户点击了“OK”按钮时,就调用ActivityCollector 的finishAll()方法销毁所有Activity ,并重新启动LoginActivity 。
再来看一下我们是怎么注册 ForceOfflineReceiver 这个BroadcastReceiver 的。
可以看到,这里重写了onResume()和onPause()这两个生命周期方法,然后分别在这两个方法里注册和取
消注册了ForceOfflineReceiver
为什么要这样写呢?之前不都是在onCreate()和onDestroy()方法里注册和取消注册BroadcastReceiver 的吗?
这是因为我们始终需要保证只有处于栈顶的Activity 才能接收到这条强制下线广播,
非栈顶的Activity 不应该也没必要接收这条广播,
所以写在onResume()和onPause()方法里就可以很好地解决这个问题,
当一个Activity 失去栈顶位置时就会自动取消BroadcastReceiver 的注册。
这样的话,所有强制下线的逻辑就已经完成了,接下来我们还需要对AndroidManifest.xml 文
件进行修改,代码如下所示
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcastbestpractice">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".LoginActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".MainActivity">
</activity>
</application>
</manifest>
这里只需要对一处代码进行修改,就是将主Activity 设置为LoginActivity ,而不再是
MainActivity ,因为你肯定不希望用户在没登录的情况下就能直接进入程序主界面吧?
现在来尝试运行一下程序吧。首先会进入登录界面,并可以在这里输入账号和密码,如图所示。
如果输入的账号是admin ,密码是123456 ,点击登录按钮就会进入程序的主界面,如图所示。
这时点击一下发送广播的按钮,就会发出一条强制下线的广播,ForceOfflineReceiver
收到这条广播后会弹出一个对话框,提示用户已被强制下线,如图所示。
这时用户将无法再对界面的任何元素进行操作,只能点击“OK”按钮,然后重新回到登录界面。
这样,强制下线功能就完整地实现了。