学更好的别人,
做更好的自己。
——《微卡智享》
本文长度为3679字,预计阅读7分钟
前言
上一篇《智能手表接收两台手机消息?最近计划》说了这个计划,将任务也做了拆解,想要备用机往主力机上推送消息,那就要先做到消息的捕获,所以本章就来做一下应用消息捕获的实现。
实现效果
从上面的GIF图中可以看到,使用微信发消息后,手机在接收到微信消息时,同时我们的APP也获取到了这条消息,并在APP程序中显示出来收到消息的内容。
微卡智享
Demo的实现
整个程序接收的核心是NotificationListenerService,用于监听Notification,然后用了datastore+liveeventbus+basequickadapter实现。
DataStore:Jetpack 组件之一,是Google 开发出来用以代替SharedPreferences实现数据存储,Demo中用于主要是存储配置需要监听的应用,毕竟不是所有的应用消息都是必须要知道的。
LiveEventBus:消息组件,这个《Android前台服务的使用(二)--使用LiveEventBus实现进程间通讯(附源码)》我用了很久了,一直觉得不错,用于在监听到Notification消息后给MainActivity通讯,让MainActivity显示出来。
BaseQuickAdapter:用了这个后,真的是节省好多代码,《Android使用BaseSectionQuickAdapter动态生成不规则宫格》正好做这个Demo的时候发现作者已经出了4.0beta版了,所以直接用了最新的beta来用。
Demo实现
微卡智享
build.gradle
dependencies {
//JepPack DataStore
implementation 'androidx.datastore:datastore-preferences:1.0.0'
implementation 'androidx.datastore:datastore-preferences-core:1.0.0'
//使用协程
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
implementation 'io.github.jeremyliao:live-event-bus-x:1.8.0'
implementation "io.github.cymchad:BaseRecyclerViewAdapterHelper:4.0.0-beta04"
}
上面是加入了DataStroe,LiveEventBus和BaseQuickAdapter的依赖项,因为我封装的DataStore类中用到了协程,所以协程的依赖也需要加进来。
AndroidManifest.xml配置权限
使用接收消息,我们需要有读取系统应用的权限,而且接收系统消息用到了NotificationListenerService,所以在application也要加入服务。
<!-- 适配安卓12&11获取当前已安装的所有应用列表-->
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
上面是读取应用列表的权限,在Android 12和11里面要加入queries
<application
android:name=".BaseApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:hardwareAccelerated="true"
android:supportsRtl="true"
android:theme="@style/Theme.NotificationDemo"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.OPENABLE" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<service
android:name=".NotificationMonitorService"
android:enabled="true"
android:label="@string/app_name"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
</application>
application下面加入了service,比较关键的是设置android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
CMessage
class CMessage {
//应用包名
var packagename = ""
//应用中文名
var appname = ""
//应用图标
var appicon: Bitmap? = null
//应用通知标题
var title = ""
//应用通知内容
var content = ""
//是否推送数据
var isMessage = true
}
定义这个类主要是我们在设置是否接收消息时一般都是显示程序的应用名,直接显示包名的话也认不出来是什么应用,而且在接收系统消息时获得的也只有包名,那可以通过包名直接提取出应用名称
NotificationMonitorService
这个是整个监听的核心,需要继承自NotificationListenerService,其中的onNotificationPosted方法是接收到新Notification消息后的回调,从参数StatusBarNotification中可以获取到消息的包名,标题及内容,从这里接收到进行处理再通过消息组件传递给Activity就可以显示出来。
package vac.test.notificationdemo
import android.app.Notification
import android.content.ComponentCallbacks
import android.content.ComponentName
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import android.util.Log
import androidx.lifecycle.Observer
import com.jeremyliao.liveeventbus.LiveEventBus
import vac.test.notificationdemo.bean.CMessage
import java.util.Hashtable
const val MESSAGE_RECV = "MESSAGE_RECV"
class NotificationMonitorService : NotificationListenerService() {
var appht = Hashtable<String, CMessage>()
override fun onListenerConnected() {
super.onListenerConnected()
Log.i("pkg", "onListenerConnected")
appht = NLSrvUtil.getAppHashTable()
}
override fun onListenerDisconnected() {
super.onListenerDisconnected()
Log.i("pkg", "onListenerDisconnected")
// 通知侦听器断开连接 - 请求重新绑定
requestRebind(ComponentName(this, NotificationListenerService::class.java))
}
//获取接收消息
override fun onNotificationPosted(sbn: StatusBarNotification?) {
super.onNotificationPosted(sbn)
sbn?.let {
val extras = it.notification.extras
//先判断是否接收消息
val isMessage = DataStoreHelper.getData(it.packageName, false)
if (isMessage) {
val NotificationMsg = appht[it.packageName]
NotificationMsg?.let { msg ->
//获取消息Title
msg.title = extras.getString(Notification.EXTRA_TITLE, "")
//获取消息内容
msg.content = extras.getString(Notification.EXTRA_TEXT, "")
LiveEventBus.get<CMessage>(MESSAGE_RECV)
.post(msg)
Log.i(
"pkg", "pkgname:${msg.packagename} " +
" title:${msg.title} text:${msg.content}"
)
}
}
}
}
override fun onNotificationRemoved(sbn: StatusBarNotification?) {
super.onNotificationRemoved(sbn)
}
}
NLSrvUtil
工具类,像获取系统应用,程序中开启和关闭系统监听都是写在这个类中的。
class NLSrvUtil {
companion object {
//获取应用列表
fun getAppList(): MutableList<CMessage> {
val applist = mutableListOf<CMessage>()
val applicationInfolist = getApplicationInfo()
for (info in applicationInfolist) {
//系统应用不获取
if ((info.flags and ApplicationInfo.FLAG_SYSTEM) == 0) {
val item = CMessage()
//获取包名
item.packagename = info.packageName
//获取App名称
item.appname = info.loadLabel(BaseApp.mContext.packageManager).toString()
//获取App图标
item.appicon = info.loadIcon(BaseApp.mContext.packageManager).toBitmap()
//标题和消息这里都默认
item.title = ""
item.content = ""
item.isMessage = DataStoreHelper.getData(item.packagename, false)
applist.add(item)
}
}
return applist
}
//获取应用列表
fun getAppHashTable(): Hashtable<String,CMessage> {
val appht = Hashtable<String, CMessage>()
val applicationInfolist = getApplicationInfo()
for (info in applicationInfolist) {
//系统应用不获取
if ((info.flags and ApplicationInfo.FLAG_SYSTEM) == 0) {
val item = CMessage()
//获取包名
item.packagename = info.packageName
//获取App名称
item.appname = info.loadLabel(BaseApp.mContext.packageManager).toString()
//获取App图标
item.appicon = info.loadIcon(BaseApp.mContext.packageManager).toBitmap()
//标题和消息这里都默认
item.title = ""
item.content = ""
item.isMessage = DataStoreHelper.getData(item.packagename, false)
appht.put(item.packagename,item)
}
}
return appht
}
private fun getApplicationInfo(): MutableList<ApplicationInfo> {
var pkglist = mutableListOf<ApplicationInfo>()
try {
pkglist =
BaseApp.mContext.packageManager.getInstalledApplications(ApplicationInfo.FLAG_INSTALLED)
} catch (e: Exception) {
Log.e("notierror", e.message.toString())
}
return pkglist
}
fun isEnable(): Boolean {
val packageNames = NotificationManagerCompat.getEnabledListenerPackages(BaseApp.mContext)
return packageNames.contains(BaseApp.mContext.packageName)
}
/**
* 切换通知监听器服务, flag 1-打开 2-关闭 其余-重启
*/
fun updateStatus(flag: Int = 0) {
val pm = BaseApp.mContext.packageManager
when(flag){
1->{
pm.setComponentEnabledSetting(
ComponentName(
BaseApp.mContext,
NotificationMonitorService::class.java
),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP
)
}
2->{
pm.setComponentEnabledSetting(
ComponentName(
BaseApp.mContext,
NotificationMonitorService::class.java
),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
)
}
else->{
pm.setComponentEnabledSetting(
ComponentName(
BaseApp.mContext,
NotificationMonitorService::class.java
),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
)
pm.setComponentEnabledSetting(
ComponentName(
BaseApp.mContext,
NotificationMonitorService::class.java
),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP
)
}
}
}
}
}
Adapter适配器
页面做的很简单,一个图标,一个TextView和一个Switch开关即可。
class MessageAdapter:BaseQuickAdapter<CMessage, MessageAdapter.VH>() {
// 自定义ViewHolder类
class VH(
parent: ViewGroup,
val binding: RclApplistBinding = RclApplistBinding.inflate(
LayoutInflater.from(parent.context), parent, false
),
) : RecyclerView.ViewHolder(binding.root)
override fun onBindViewHolder(holder: VH, position: Int, item: CMessage?) {
item?.let {
holder.binding.rclAppicon.setImageBitmap(it.appicon)
holder.binding.rclAppname.setText(it.appname)
holder.binding.rclIsmsg.isChecked = it.isMessage
}
}
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
// 返回一个 ViewHolder
return VH(parent)
}
}
BaseQuickAdapter4.0后的写法和3.0也变了好多,以往版本不同,不再提供其他复杂功能(例如“加载更多”),目前仅包含“空布局”、数据操作、点击事件功能。是作为一个基本的、简单的、单纯的基础类存在,定义更加明确,不再是大杂烩。
为了支持官方的ConcatAdapter使用,ViewHolder不得不提出来,由开发者自行实现,表面上看似乎是增加繁琐程度,但从长远来看,实际上提供了更高的灵活度。我还是很喜欢有更高灵活度的东西。
MainActivity
最后就剩下MainActivity了,这里面主要是程序启动时的消息服务监听开启,Adapter修改是否监听时的事件,再就是方便看到接收的消息,我在TextView接收到消息后加入了一个放大的动画特效。
const val ISLISTENMSG = "isListenMsg"
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
//监听开关
private var isListened = false
private lateinit var msgAdapter: MessageAdapter
private var msgList = mutableListOf<CMessage>()
val getContent =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { rst: ActivityResult? ->
// Handle the returned Uri
rst?.let {
if (!NLSrvUtil.isEnable()) {
Log.i("pkg", "NLSrv Stop")
NLSrvUtil.updateStatus(2)
} else {
Log.i("pkg", "NLSrv Start")
NLSrvUtil.updateStatus()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
//监听开关按钮
isListened = DataStoreHelper.getData(ISLISTENMSG, false)
Log.i("pkg", "NLSrv ${isListened}")
val status = if (isListened) 0 else 2
NLSrvUtil.updateStatus(status)
binding.isListenMsg.setOnCheckedChangeListener { compoundButton, ischecked ->
if (ischecked) {
if (!NLSrvUtil.isEnable()) {
getContent.launch(Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"))
} else {
isListened = true
Log.i("pkg", "NLSrv Start")
NLSrvUtil.updateStatus()
DataStoreHelper.putData(ISLISTENMSG, isListened)
}
} else {
isListened = false
NLSrvUtil.updateStatus(2)
DataStoreHelper.putData(ISLISTENMSG, isListened)
}
}
binding.isListenMsg.isChecked = isListened
msgList = NLSrvUtil.getAppList()
msgAdapter = MessageAdapter()
msgAdapter.submitList(msgList)
msgAdapter.addOnItemChildClickListener(R.id.rcl_ismsg) { adapter, view, position ->
Log.i("pkg", "click:${position}")
if (view.id == R.id.rcl_ismsg) {
val switch = view as Switch
var item = adapter.getItem(position)
item?.let {
it.isMessage = view.isChecked
DataStoreHelper.putData(it.packagename, it.isMessage)
}
}
}
val layoutManager = LinearLayoutManager(this)
binding.recyclerView.layoutManager = layoutManager
binding.recyclerView.adapter = msgAdapter
LiveEventBus.get(MESSAGE_RECV, CMessage::class.java)
.observe(this) { t ->
t?.let {
binding.tvmsg.text = "${it.appname}\r\n${it.title}\r\n${it.content}"
binding.tvmsg.startAnimation(getAnimation())
}
}
}
//设置动画
fun getAnimation(): AnimationSet {
val animation = AnimationSet(true)
val scale = ScaleAnimation(
1f, 3f, 1f, 3f, RELATIVE_TO_SELF,
0.5f, RELATIVE_TO_SELF, 0.5f
)
scale.repeatCount = 1
scale.repeatMode = Animation.REVERSE
animation.addAnimation(scale)
//设置动画时长为1秒
animation.duration = 400
animation.repeatMode = Animation.REVERSE
//设置插值器为先加速再减速
animation.interpolator = AccelerateDecelerateInterpolator()
//动画完成后保持位置
animation.fillAfter = false
//保持动画开始时的状态
animation.fillBefore = true
//取消动画
animation.cancel()
//释放资源
animation.reset()
return animation
}
}
微卡智享
关键点
1.消息监听服务需要应用开启权限才能使用,手机不同开启的地方应该也不一样,我是OPPO的是在应用管理--特殊应用权限--设备和应用通知中开启
2.程序每次打开都要先做一次关闭再开启,否则会接收不到消息。而程序在运行中修改接收的消息,我现在使用的是修改本地存储DataStore,然后处理接收时直接读本地DataStore来判断是否推送。
这样一个系统监听消息的程序就实现了,像手表想要接收电话和短信,这个是无法实现的,下一篇我们就来实现一下电话和短信的监听也有通知显示。
完
往期精彩回顾