Android监听消息(一)——应用消息捕获

学更好的别人,

做更好的自己。

——《微卡智享》

7723701b20da1f07ed77c16145656073.jpeg

本文长度为3679,预计阅读7分钟

前言

上一篇《智能手表接收两台手机消息?最近计划》说了这个计划,将任务也做了拆解,想要备用机往主力机上推送消息,那就要先做到消息的捕获,所以本章就来做一下应用消息捕获的实现。

2bb1a69856098b48d50eb1f04d3b4a71.png

实现效果

db71071fbde293cf7dae48b48b98a7b2.jpeg

a25c9feb54bf801d70f1642b5a7fc239.gif

从上面的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实现

4348f5008133b042ad3e59aea2a05f41.png

微卡智享

fe629ca12875f6bcfbd05ca2d114e794.png

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适配器

613da8dc178e1066210c0de73eb78f8d.png

页面做的很简单,一个图标,一个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的是在应用管理--特殊应用权限--设备和应用通知中开启

f9339e83c9adcfec8faaf952dcb90891.png

2.程序每次打开都要先做一次关闭再开启,否则会接收不到消息。而程序在运行中修改接收的消息,我现在使用的是修改本地存储DataStore,然后处理接收时直接读本地DataStore来判断是否推送。

6a6faaf1acf110ef5ef535d1e8873143.png

这样一个系统监听消息的程序就实现了,像手表想要接收电话和短信,这个是无法实现的,下一篇我们就来实现一下电话和短信的监听也有通知显示。

72b1d89598db8b68b45598b320f38ff4.png

1c0fc1941376ad7f148ed1686ec4ef7f.png

往期精彩回顾

 

5bb95da02cf4b49049713745f15a50b9.jpeg

智能手表接收两台手机消息?最近计划

 

 

dd701fe78a21614125ab082a33a1a7ce.jpeg

测试新版Android Studio的手机镜像效果

 

 

f455d4755a390b38543aa41b9232ee32.jpeg

yolov5训练自己的数据集,OpenCV DNN推理

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vaccae

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值