本文已参与「新人创作礼」活动.一起开启创作之路。

软件环境:

  • Jetbrains Toolbox
  • Android Sudio 2021.1.1 Bumblebee
  • JDK 17.0.2

请先参考前一篇文章复习一下Kotlin的一些语法。

配套代码获取地址:

Gitee直接下载,全部开源

广播机制简介

Android的每一个应用程序可以选择自己需要的广播来接收,可以是来自于系统的广播内容,也可以是来自某个应用程序的广播内容,下面介绍一下Android的广播的类型:

广播的类型

BroadcastReceiver是Android应用程序接收的主要实现类,他是每一个应用程序的广播接收者。

  • 标准广播:

是一种完全异步的执行的广播,在广播发出以后,所有的BroadcastReceiver几乎会在同一个时刻收到这条广播,因此,这种广播没有任何顺序可言。效率较高,但无法截断。

广播流程示意图:

Android开发笔记——快速入门(全局大喇叭)

  • 有序广播:

是一种完全同步执行的广播,在广播发出以后,同一个时刻,只会有一个BroadcastReceiver能够接收到这条广播信息,当这个BroadcastReceiver中的逻辑执行完毕以后,广播才会向下一个继续传递,所以这个广播是有前后顺序的,并且前边的BroadcastReceiver可以把广播截断这样后边的BroadcastReceiver就无法接收到消息了。

流程示意图如下:

Android开发笔记——快速入门(全局大喇叭)

从接收系统广播开始

Android系统内置了很多广播,开机的时候就会自动发送一条广播,电池电量发生变化的时候会再发出来一条广播,系统时间发生变化的时候也会发出对应的广播,如果要接收这些广播,就需要使用BroadcastReceiver。

动态注册监听时间变化

在使用BroadcastReceiver前需要注册ManiFest,注册的方法有两种;

  • 在代码中注册。
  • 在ManiFest中注册。

我们先来讲解如何在代码中注册。

动态注册BroadcastReceiver

我们新建一个项目,包含一个空的Act并将项目的名字设置为BroadcastReceiver。

如何创建一个BroadcastReceiver?

只需要新建立一个类让他继承BroadcastReceiver就可以了,重写BroadcastReceiver的onReceiver方法,当有广播来的时候就会调用此方法进行处理。

修改MainActivity的代码如下:

class MainActivity : BaseActivity() {
  lateinit var timeChangeReceiver : TimeChangeReceiver
​
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
​
    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 is changed",Toast.LENGTH_SHORT).show()
     }
   }
}

这段代码看起来很复杂实际上思路很清晰,我们先从下边的TimeChangeReceiver类说起

inner class TimeChangeReceiver : BroadcastReceiver()
{
  override fun onReceive(context: Context?, intent: Intent?) {
    Toast.makeText(context,"Time is changed",Toast.LENGTH_SHORT).show()
   }
}

就像上文说的我们继承于BroadcastReceiver()类,并且重写了对应的onReceive方法,让他在收到系统的广播之后显示一个Toast表示时间变化了。

 override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
​
    val intentFilter = IntentFilter()
    intentFilter.addAction("android.intent.action.TIME_TICK")
     timeChangeReceiver = TimeChangeReceiver()
    registerReceiver(timeChangeReceiver,intentFilter)
   }

在onCreate方法之中我们通过这个 IntentFilter()方法构造了一个intent-Filter如果你不太清楚intent-Filter可以先去看看。

我们通过过滤器设置来设置接受的广播信息,其中android.intent.action.TIME_TICK就是Android发出的时间变化广播。

再将刚才声明的TimeChangeReceiver实例化,通过Context的函数registerReceiver来将这个过滤器和这个Act绑定到一起。最终实现在这个Act中时间变化广播的接收。

最后我们在onDestory方法中取消这个Receiver的注册:

这里调用的同样也是onContext里的方法。

  override fun onDestroy() {
    super.onDestroy()
    unregisterReceiver(timeChangeReceiver)
   }

静态注册注册BroadcastReceiver

动态注册可以自由地注册与控制BoardcastReceiver,但是缺点也很明显,就是无法在不启动应用的情况下相应广播,要想实现这个功能就要使用静态注册的方式。

理论上来说,系统的广播不论在动态的BroadcastReceiver或者静态的BroadcastReceiver都应该能接收到对应的信息,但是在Android 8.0以后为了防止应用通过静态注册频繁自启动,就将静态注册获取到隐式广播的功能砍掉了,而大多数系统广播实际上是隐式广播,不过还好还留下了部分系统广播作为非隐式广播。

接下来我们演示如何通过静态广播实现应用自启动。

我们首先通过AS的引导来创建一个BroadcastReceiver:

Android开发笔记——快速入门(全局大喇叭)

其中,Exported代表是否允许这个BroadcastReceiver接收本程序以外的广播,Enabled代表是否启用这个BroadcastReceiver,修改名字为BootCompleteReceiver后完成创建。

修改类的代码如下:

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_SHORT).show()
  }
}

内容很简单就是在接收到广播的时候发出一条toast提示启动。

需要注意的是静态的BroadcastReceiver需要注册在Manifest的文件里面才可以使用,不过我们是通过引导的方式创建的,所以AS已经自动帮我们注册好了,是不是很方便?

我打开看一下:

Android开发笔记——快速入门(全局大喇叭)

出现了一个新的标签receiver,所有的BroadcastReceiver都是在这里注册的,他的注册方法和< Fragment>其实很相似,也是通过android:name=".BootCompleteReceiver"来指定绑定的是哪一个BroadcastReceiver。

不过需要注意的是我们没有指定这个BroadcastReceiver具体接收哪一个广播所以还是通过intent-filter来指定接受的广播。

修改后如下:

<receiver
  android:name=".BootCompleteReceiver"
  android:enabled="true"
  android:exported="true" >
  
<intent-filter>
  <action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
  
</receiver>

与其他广播不同的是有些系统广播是有很严格的权限要求的,这里不再详细赘述,只知道要在Manifest中添加一个权限声明获取即可:

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

实测在荣耀magic40 最新版本下,Android12是无法获取到对应的广播的,但是在模拟器的原生google就可以:

Android开发笔记——快速入门(全局大喇叭)

发送自定义广播

在使用BroadcastReceiver接收广播以后,我们再来尝试使用如何发送广播。

发送标准广播

为了测试接收到的广播我们再来准备一个BroadcastReceiver作为接受对象,我们新建立一个类叫MyBroadcastReceiver,并在Mainfest里面添加这个BroadcastReceiver。

创建类代码入下:

class MyReceiver : BroadcastReceiver() {
​
  override fun onReceive(context: Context, intent: Intent) {
    // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
    Toast.makeText(context,"Received the message",Toast.LENGTH_SHORT).show()
    
   }

对应的Mainfest添加代码如下:

<receiver
  android:name=".MyReceiver"
  android:enabled="true"
  android:exported="true">
  <intent-filter>
    <action android:name="android.intent.action.MY_BOARDCAST" />
  </intent-filter>
</receiver>

这里设置让其接收一个android.intent.action.MY_BOARDCAST广播,等下就要发送这样一条广播。

在布局中再添加一个button如下:

<Button
  android:id="@+id/button"
  android:layout_width="0dp"
  android:layout_height="wrap_content"
  android:text="Send a boardcast"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toTopOf="parent" />

效果如下:

Android开发笔记——快速入门(全局大喇叭)

然后在MainActivity中调用viewbinding来获取到对应button的实例。

class MainActivity : BaseActivity() {
  lateinit var timeChangeReceiver : TimeChangeReceiver
  lateinit var binding: ActivityMainBinding
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
​
    val intentFilter = IntentFilter()
    intentFilter.addAction("android.intent.action.TIME_TICK")
     timeChangeReceiver = TimeChangeReceiver()
    registerReceiver(timeChangeReceiver,intentFilter)
​
    /*Send a boardcast*/
    binding=ActivityMainBinding.inflate(layoutInflater)
    binding.button.setOnClickListener {
      val intent = Intent("android.intent.action.MY_BOARDCAST")
      intent.setPackage(packageName)
      sendBroadcast(intent)
     }
    setContentView(binding.root)
   }
   .......
}

在这一段中我们给button添加了一个点击回调函数:

/*Send a boardcast*/
binding=ActivityMainBinding.inflate(layoutInflater)
binding.button.setOnClickListener {
  val intent = Intent("android.intent.action.MY_BOARDCAST")
  intent.setPackage(packageName)
  sendBroadcast(intent)
}
setContentView(binding.root)

首先我们创建了一个Intent指明了发送广播的意图,这里用的就是上文注册的广播,然后通过setPackage方法指定发送的应用名称,通过packageName来获取到本应用的包名。最后调用sendBroadcast(intent) 方法发送广播,这里需要注意的是,在默认情况下,如果你不用setPackage指明是哪一个应用,那么他就无法发送,因为上文提到过,没有指明包名的话它默认的是一个匿名的广播,而目前的系统是禁止发送匿名广播的。

最后实现效果,点击button:

Android开发笔记——快速入门(全局大喇叭)

发送有序广播

发送有序广播的方法很简单,和标准广播几乎类似,为了验证标准广播是可以被截断的,我们再创建一个和上一小节一样的BroadcastReceiver来接收消息。

代码如下所示:

package com.example.boardcastreceiver
​
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
​
class MyReceiver2 : BroadcastReceiver() {
​
  override fun onReceive(context: Context, intent: Intent) {
    // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
    Toast.makeText(context,"Received the message 2", Toast.LENGTH_SHORT).show()
​
   }
}

创建完成以后记得修改mainfest

<receiver
  android:name=".MyReceiver2"
  android:enabled="true"
  android:exported="true">
  <intent-filter>
    <action android:name="android.intent.action.MY_BOARDCAST" />
  </intent-filter>
</receiver>

到这一步发送的还是标准广播。两个Receiver依次把内容显示出来。

想要改成有序广播很简单,直接修改发送的函数即可。

代码如下:

/*Send a boardcast*/
binding=ActivityMainBinding.inflate(layoutInflater)
binding.button.setOnClickListener {
  val intent = Intent("android.intent.action.MY_BOARDCAST")
  intent.setPackage(packageName)
  sendOrderedBroadcast(intent,null)
}
setContentView(binding.root)

sendOrderedBroadcast(intent,null)就能发送对应的有序广播,第二个参数是一个与权限相关的字符串,这里先不讨论。

Android开发笔记——快速入门(全局大喇叭)

Android开发笔记——快速入门(全局大喇叭)

我先说如何变成有序:

修改mainfest的代码曾加第一个receiver的优先级

<receiver
  android:name=".MyReceiver"
  android:enabled="true"
  android:exported="true">
  <intent-filter android:priority="100">
    <action android:name="android.intent.action.MY_BOARDCAST" />
  </intent-filter>
</receiver>

这时候谁的优先级大谁就能优先接收到对应的消息。

那么如何截断消息呢?

也很简单直接在对应的BroadcastReceiver类中修改onReceive方法即可:

class MyReceiver : BroadcastReceiver() {
​
  override fun onReceive(context: Context, intent: Intent) {
    // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
    Toast.makeText(context,"Received the message",Toast.LENGTH_SHORT).show()
    abortBroadcast()
   }
}

这时候就能截断消息了,方法还算很简单。

我们在来测试一下,发现只能接收一个消息了而且还只能是第一个Receiver。

Android开发笔记——快速入门(全局大喇叭)

最佳实现——强制下线功能

强制下线的功能是一个很常见的功能,在某些时刻你的账号的异地登陆了,就需要将目前在线的内容强制下线,从技术层面上来讲,强制下线就是弹出一个对话窗,不管用户选择哪一个选项,都将退出当前的Activity,返回至登录界面。

下面我们来实现一下这个功能:

首先要实现的功能是在任意的Activity中都能退出程序,这个功能的实现方式已经在Activity讲过了,这里就不再详细了。

我们先来实现一个登陆界面:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  tools:context=".MainActivity">
​
  <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>

效果如下:

Android开发笔记——快速入门(全局大喇叭)

我们在登陆按钮添加如下逻辑:

/*login*/
binding=ActivityMainBinding.inflate(layoutInflater)
binding.login.setOnClickListener {
  val account = binding.accountEdit.text.toString()
  val password = binding.passwordEdit.text.toString()
  
  if(account == "admin" && password =="123") {
    val intent = Intent(this,OfflineActivity::class.java)
    startActivity(intent)
    finish()
   }else{
    Toast.makeText(this,"account or password is invalid",Toast.LENGTH_SHORT).show()
   }
}

实现的内容很简单,就是读取账户和密码来校验一下,登陆成功后开启另一个OffilineActivity,并将这个Activity关闭。

我们再来看一下登陆后的Activity中有什么内容:

首先布局很简单:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".OfflineActivity">
​
  <Button
    android:id="@+id/button"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="OFFLINE"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

只包含一个用于下线的按钮。

Android开发笔记——快速入门(全局大喇叭)

为按钮添加一个回调,当点击的时候就会发出一个广播让对应的BroadcastReceiver来处理当前的事件。

代码如下:

class OfflineActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    lateinit var binding: ActivityOfflineBinding
    super.onCreate(savedInstanceState)
    setContentView(binding.root)
    binding.button.setOnClickListener {
      val intent = Intent("android.intent.action.MY_BOARDCAST")
      sendBroadcast(intent)
     }
   }
}

终于到了实现强制下线功能的核心阶段了,我们需要考虑的有两个方面,首先是

我怎么样才能弹出对应的对话框呢?单纯的一个静态的BroadcastReceiver是无法弹出任何一个UI组件的,因为不管哪个UI都需要在一定的context来构建,而在一个单独的静态的Receiver里面的context实际上是他自身,不信输出一下看看:

override fun onReceive(context: Context, intent: Intent) {
  // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
  Toast.makeText(context,context.toString(),Toast.LENGTH_SHORT).show()
}

可以看到对应的context实际上是不对的,无法为UI提供支持:

Android开发笔记——快速入门(全局大喇叭)

那么应该怎么办呢?也很简单我们在BaseActivity中动态注册一个receiver这样所有的Act中都包含了一个receiver。

动态注册的办法在前面已经讲得很详细了,这里就不在多讨论了直接看代码。

open class BaseActivity : AppCompatActivity() {
​
  val tag :String = javaClass.simpleName
  lateinit var receiver:OfflineReceiver
​
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d(tag,"onCreate")
    ActivityCollector.addActivity(this)
   }
   ........
  override fun onResume() {
    super.onResume()
    val intentFilter = IntentFilter()
    intentFilter.addAction("android.intent.action.MY_BOARDCAST")
    receiver = OfflineReceiver()
    registerReceiver(receiver,intentFilter)
    Log.d(tag, "onResume")
   }
  override fun onPause() {
    super.onPause()
    unregisterReceiver(receiver)
    Log.d(tag, "onPause")
   }
   ............
  inner class OfflineReceiver:BroadcastReceiver() {
​
    override fun onReceive(context: Context, intent: Intent?) {
      AlertDialog.Builder(context).apply {
        setTitle("WARNING")
        setMessage("FORCE TO OFFLINE")
        setCancelable(false)
        setPositiveButton("OK") { _, _
          ->
          ActivityCollector.finishALL()
          val i = Intent(context, MainActivity::class.java)
          context.startActivity(i)
         }
        show()
       }
     }
   }
​
}

在这里我们实现了在onReceive方法里面为当前界面注册了一个AlertDialog,其中这里的context实际上是:

Android开发笔记——快速入门(全局大喇叭)

基于Activity的,这样就能生成一个DIALOG了。

但是具体是为什么,静态和动态的具体差距还不清楚,有时间再研究一下。