Android监听消息(二)——电话及短信监听

news/2024/4/24 23:20:59/

学更好的别人,

做更好的自己。

——《微卡智享》

541ca03168e8b1b77e905f275152c364.jpeg

本文长度为2747,预计阅读6分钟

前言

前面一篇《Android监听消息(一)——应用消息捕获》我们使用NotificationListenerService实现了应用的消息监听,但是电话和短信是接收不到的,所以这一篇我们就来解决怎么监听电话及短信,电话主要就是在响铃时发送来电人消息,短信的话是捕获到消息内容直接发送出来。

fcd000ebcbe34f08ffddc3fa1a2019a6.png

微卡智享

实现思路

在Android中实现电话捕获用到的还是TelephonyManager,在Android12前TelephonyManager可以使用PhoneStateListener来进行监听,里面的onCallStateChanged可以直接获取到来电状态和来电号码,比较方便,如下图:

30380aba28c920f27dd27bd352f915fc.png

但是在Android12(sdk31)后,listen已经不能用了,需要使用registerTelephonyCallback来获取到来电状态,但是这里无法获取到来电号码了,所以为了实现接到到来电状态并能获取到来电人的信息,需要使用BroadcastReceiver来实现。

而捕获短信的消息则是使用android.telephony.SmsMessage,并且短信的接收也是通过BroadcastReceiver来实现,这样我们就把电话和短信接收直接写在一个BroadcastReceiver中即可。

代码实现

394da72110f59a41bc8e4bca493aac8e.png

微卡智享

01

权限申请

接着上一篇的Demo,我们在这个基础上再增加相关的设置,首先要捕获电话及短信,那相关的权限必须要先申请出来。

1c82ccfe8ad2c5dac39ba2d06b5ec97f.png

<uses-permission android:name="android.permission.ACTION_NOTIFICATION_LISTENER_SETTINGS" /><uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" /><uses-permission android:name="android.permission.READ_PHONE_STATE" /><uses-permission android:name="android.permission.READ_PHONE_NUMBERS" /><uses-permission android:name="android.permission.CALL_PHONE" /><uses-permission android:name="android.permission.RECEIVE_SMS" /><uses-permission android:name="android.permission.SEND_SMS" /><uses-permission android:name="android.permission.READ_SMS" /><uses-permission android:name="android.permission.READ_CALL_LOG" /><uses-permission android:name="android.permission.WRITE_CALL_LOG" /><uses-permission android:name="android.permission.READ_CONTACTS" /><uses-permission android:name="android.permission.WRITE_CONTACTS" />

在Manifest中加入权限申请,当然Android6.0后需要动态申请权限了,所以要在MainActivity中加入动态申请权限。

//权限申请companion object {private const val REQUEST_CODE_PERMISSIONS = 10private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS,Manifest.permission.ANSWER_PHONE_CALLS, Manifest.permission.CALL_PHONE,Manifest.permission.RECEIVE_SMS,Manifest.permission.SEND_SMS,Manifest.permission.READ_SMS, Manifest.permission.READ_CALL_LOG,Manifest.permission.WRITE_CALL_LOG, Manifest.permission.READ_CALL_LOG,Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)}private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {ContextCompat.checkSelfPermission(BaseApp.mContext, it) == PackageManager.PERMISSION_GRANTED}override fun onRequestPermissionsResult(requestCode: Int,permissions: Array<out String>,grantResults: IntArray) {super.onRequestPermissionsResult(requestCode, permissions, grantResults)if (requestCode == REQUEST_CODE_PERMISSIONS) {if (allPermissionsGranted()) {//监听开关按钮isListened = DataStoreHelper.getData(ISLISTENMSG, false)Log.i("pkg", "NLSrv ${isListened}")val status = if (isListened) 0 else 2NLSrvUtil.updateStatus(status)} else {Toast.makeText(this, "未开启权限.", Toast.LENGTH_SHORT).show()finish()}}}//onCreate中再加入申请权限的动作override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)//申请权限if (!allPermissionsGranted()) {requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)}}

02

电话及短信的BroadcastReceiver

这个广播接收是这篇的一个重点,先上代码:

package vac.test.notificationdemoimport android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Telephony
import android.service.notification.NotificationListenerService
import android.telecom.TelecomManager
import android.telephony.PhoneStateListener
import android.telephony.SmsMessage
import android.telephony.TelephonyCallback
import android.telephony.TelephonyManager
import android.util.Log
import com.jeremyliao.liveeventbus.LiveEventBus
import vac.test.notificationdemo.bean.CMessage
import java.util.Hashtable
import java.util.Objectsclass PhoneStateReceiver : BroadcastReceiver() {//当前来电号码var mPhoneNum: String? = nullvar mLastPhoneNum: String? = null//当前短信号码var mLastSmsPhoneNum: String? = nullvar mLastSmsContent: String? = nullval contactsht: Hashtable<String, String> = NLSrvUtil.getContactsHashTable()var telMng: TelephonyManager =BaseApp.mContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManagerfun createPhone(phonenum: String?): CMessage {val msg = CMessage()msg.packagename = "来电"msg.appname = "来电"msg.title = contactsht[phonenum] ?: "未知号码"msg.content = phonenum ?: "未知号码"return msg}fun createSms(phonenum: String?, content:String?): CMessage {val msg = CMessage()msg.packagename = "短信"msg.appname = "短信"msg.title = contactsht[phonenum] ?: "未知号码"msg.content = content ?: "未解析内容"return msg}init {telMng = BaseApp.mContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManagerif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {telMng.registerTelephonyCallback(BaseApp.mContext.mainExecutor,object : TelephonyCallback(), TelephonyCallback.CallStateListener {override fun onCallStateChanged(state: Int) {when (state) {TelephonyManager.CALL_STATE_IDLE -> {Log.d("pkg", "挂断")mLastPhoneNum = nullmPhoneNum = null}TelephonyManager.CALL_STATE_OFFHOOK -> {Log.d("pkg", "接听")mLastPhoneNum = nullmPhoneNum = null}TelephonyManager.CALL_STATE_RINGING -> {Log.d("pkg", "CALL_STATE_RINGING")mPhoneNum?.let {if (mLastPhoneNum != it) {mLastPhoneNum = itval msg = createPhone(mPhoneNum)LiveEventBus.get<CMessage>(MESSAGE_RECV).post(msg)Log.d("pkg", "响,号${mPhoneNum}")}}}}}})} else {telMng.listen(object : PhoneStateListener() {override fun onCallStateChanged(state: Int, phoneNumber: String?) {when (state) {TelephonyManager.CALL_STATE_IDLE ->Log.d("log", "挂断")TelephonyManager.CALL_STATE_OFFHOOK ->Log.d("log", "接听")TelephonyManager.CALL_STATE_RINGING -> {Log.d("log", "响铃,来电号码:${phoneNumber}")val msg = createPhone(phoneNumber)LiveEventBus.get<CMessage>(MESSAGE_RECV).post(msg)}}}}, PhoneStateListener.LISTEN_CALL_STATE)}}override fun onReceive(context: Context, intent: Intent) {Log.d("pkg", "Action:${intent.action}")when (intent.action) {//监听电话状态"android.intent.action.PHONE_STATE" -> {mPhoneNum = intent.extras?.getString("incoming_number")Log.d("pkg", "号码:${mPhoneNum}")}Telephony.Sms.Intents.SMS_RECEIVED_ACTION -> {var curphonenum: String? = nullval content = StringBuilder()val smsbundle = intent.extrasval format = intent.getStringExtra("format")smsbundle?.let {val pdus = smsbundle.get("pdus") as Array<*>pdus?.let {for (item in it) {val message = SmsMessage.createFromPdu(item as ByteArray, format)//短信电话号码curphonenum = message.originatingAddresscontent.append(message.messageBody)val mills = message.timestampMillisval status = message.statusLog.i("pkg", "phonenum:${curphonenum}, mills:${mills}, status:${status}")}}}//判断相同的消息就不再发送,防止接收过多if(curphonenum == mLastSmsPhoneNum && content.toString() == mLastSmsContent) return//记录最后一次接收短信的号码和内容mLastSmsPhoneNum = curphonenummLastSmsContent = content.toString()Log.i("pkg", "phone:${mLastSmsPhoneNum},Content:${mLastSmsContent}")val msg = createSms(mLastSmsPhoneNum,mLastSmsContent)LiveEventBus.get<CMessage>(MESSAGE_RECV).post(msg)}}}
}

25a090ce35647e6bae97333ce60f4820.png

我的Demo程序使用的sdk是33,,TelephonyManager中使用registerTelephonyCallback来注册监听电话,上图红框中TelephonyManager.CALL_STATE_RINGING代表着响铃,也就是这个状态时直接使用LiveEventBus进行消息通讯。

af11771b5048bc0193133640023bbf89.png

定义了四个变量,主要是用于处理当前响铃的电话及收到短信的信息和号码,因为在测试过程中,使用广播接收时可能会触发多次,所以这里定义了变量用于处理多次接收相同的不再重复推送消息。

e52a8a3317e6356b949d7b788a527d35.png

通过修改onReceive来判断是电话还是短信,短信中也加入了相同信息不再重复推送。

03

关于联系人信息

使用BroadcastReceiver接收到的都是来电的号码,现在很少有人去背号码了,所以这里需要将号码转换成联系人的信息再推送过来。

87e0f253f029820324c44f94c2fc0ee5.png

我们在Demo的工具类中加入了一个getContactsHashTable的函数,用于导出联系人信息存放于HashTable中,这样通过号码查找也快。

//获取联系人信息fun getContactsHashTable() : Hashtable<String,String> {val contactsht = Hashtable<String, String>()var cursor: Cursor? = nulltry{cursor = BaseApp.mContext.contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null)cursor?.let {while (it.moveToNext()){val phonename = it.getString(it.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))val phonenum = it.getString(it.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))contactsht.put(phonenum, phonename)}}}catch (e: Exception) {e.printStackTrace()}finally {cursor?.let {it.close()}}return contactsht}

54049e135fa85922225a05b1cf059ac1.png

而在PhoneStateReceiver中直接再写两个函数,通过来电号码和短信相关信息直接生成我们的CMessage类,再进行消息组件的通讯,这样电话和短信的接收通讯也就可以实现了。测试的来电和短信图片我就不再发上来了,主要是还要P图麻烦。

Tips

消息监听和模拟推送的Demo就已经完成了,测试正常,因为即然是想实时监听,就要保证锁屏也能正常使用,原来我考虑要再做成前台服务,不过我在手机系统中加入耗电设置后,也一直能实现锁屏的监听情况了,所以前台服务就暂时不加入进去了,有必要的时候再考虑。

另外就是应用把锁定也勾选上,这样杀后台的时候也不会将当前应用杀掉,正好做了下测试,手机待机一晚上,第二天再发消息,还是能正常接收消息,说明应用程序一直在后台运行中。

be8dd36d3766250e472709801dd90534.png

我的是Oppo Find N2 Flip,在设置的电池中,把当前应用的允许完成后台行为打上勾后,监听可以一直正常没问题。监听这块Demo基本就告一段落了,下一步就要开始做蓝牙通讯的相关Demo,用于监听到消息后的手机间通讯。

66000154d5977246471580c86f74e8a8.png

84d9d76a2a577fbde57c960f1be35295.png

往期精彩回顾

 

0ac42a4389cd848df4e9159a33774a48.jpeg

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

 

 

ca12fafca6e7f41a0e4815ed3ff81a94.jpeg

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

 

 

8cc270def9c21dc3da3ffc1e05c4aa59.jpeg

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

 


http://www.ppmy.cn/news/48149.html

相关文章

WPF教程(二)--Application WPF程序启动方式

1.Application介绍 WPF与WinForm一样有一个 Application对象来进行一些全局的行为和操作&#xff0c;并且每个 Domain &#xff08;应用程序域&#xff09;中仅且只有一个 Application 实例存在。和 WinForm 不同的是WPF Application默认由两部分组成 : App.xaml 和 App.xaml.…

软考-项目管理科学基础(二十一)

第21章项目管理科学基础 (P607考5分择) 21.1.2项目经济评价 根据是否考虑资金的时间价值&#xff0c;投资项目经济评价方法可分为两类:静态评价和动态评价。 1.静态评价方法 静态评价是指在进行项目方案效益和费用的计算时&#xff0c;不考虑资金的时间价值&#xff0c;不计利…

CMD命令学习整理

01、定时关机 倒计时&#xff1a;shutdown -s -t 3600 解释&#xff1a;-s表示本机&#xff1b;-t 3600表示3600秒后。意思就是本台电脑在一个小时后关机 at 12:00 shutdown -s 表示本台电脑将在12点关机。大家可以按照自己的需求设置。 取消&#xff1a;shutdown -a 取消关…

设计模式-行为型模式之中介者模式

2. 中介者模式 2.1. 模式动机 在用户与用户直接聊天的设计方案中&#xff0c;用户对象之间存在很强的 关联性&#xff0c;将导致系统出现如下问题&#xff1a; 系统结构复杂 对象之间存在大量的相互关联和调用&#xff0c;若有一个对象发生变化&#xff0c;则需要跟踪和该对象关…

苹果撤离中国市场?中国市场太重要,印度制造是备胎

苹果在中国之行后&#xff0c;却计划进一步扩大印度制造iPhone的比例&#xff0c;甚至将iPhone15全数交给印度制造&#xff0c;业界因此认为苹果正在离开中国市场&#xff0c;然而这完全是臆想&#xff0c;中国市场对苹果来说仍然非常重要&#xff0c;它不会轻易舍弃这个市场。…

进程和线程的区别

进程和线程的区别 进程线程线程和进程的区别堆栈空间安全性通信机制 什么时候使用进程和线程线程和进程之间的关系多线程一般用在哪些方面多线程的优点多线程的缺点 进程 资源分配的最小单元 线程 程序执行的最小单元 系统分配处理器时间的基本单元 线程和进程的区别 堆栈…

( “树” 之 BFS) 637. 二叉树的层平均值 ——【Leetcode每日一题】

637. 二叉树的层平均值 给定一个非空二叉树的根节点 root , 以数组的形式返回每一层节点的平均值。与实际答案相差 1 0 − 5 10^{-5} 10−5 以内的答案可以被接受。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;[3.00000,14.50000,1…

性能优化3-分帧寻路+寻路任务统一管理

前言 当项目里的地图越来越大&#xff0c;一些性能上的问题开始逐渐出现&#xff0c;比如寻路。玩家在操控角色移动的时候&#xff0c;指引需要实时更新&#xff0c;同时一些npc也需要做移动&#xff0c;容易出现cpu占用率短时间过高&#xff0c;甚至掉帧的情况。 去年底的时候…

java基于mvc的停车收费系统mysql

系统需要解决的主要问题有&#xff1a; (1)车位管理模块 添加车位、查看车位状态、车位信息查询等。 (2)客户信息管理模块 客户基本信息录入、客户信息查询等。 (3)卡业务办理 添加卡信息、查余额查询、卡充值。 (4)车辆信息管理模块 车牌信息录入等。 (5)收费管理 可以调整相应…

<a> 元素相关属性及方法

<a> 元素 <a>元素用来设置链接。除了网页元素的通用接口&#xff08;Node接口、Element接口、HTMLElement接口&#xff09;&#xff0c;它还继承了HTMLAnchorElement接口和HTMLHyperlinkElementUtils接口。 目录 属性 URL 相关属性accessKey 属性download 属性href…

linux及openEuler破解root密码

第一步&#xff1a;开机的时候按键盘的字母 E 键&#xff0c; 进入引导模式 第二步&#xff1a;进入引导模式 &#xff1a;找到linux这一行&#xff0c;按键盘上的end 键&#xff0c;跳转到行尾&#xff0c;输入&#xff1a; init/bin/sh 修改完后&#xff0c;按键盘上的 ctr…

上海车展:比亚迪宋L概念车全球首发,这是要硬扛特斯拉?

纵观2023年的新能源汽车市场&#xff0c;特斯拉可以说当仁不让地成为了全球最为“吸睛”的车企之一。凭借一系列令无数人瞠目结舌的降价举措&#xff0c;特斯拉给全球汽车市场带来了强烈冲击。虽然特斯拉上海工厂已经接近满负荷运转&#xff0c;但是面对雪片般飞来的订单依然供…

python学习——缺失值、重复值处理、排序及替换

文章目录 1 缺失值处理1.1 查看缺失值 df.isnull()1.2 统计缺失值 df.isnull().sum()1.3 删除缺失值 df.drop()1.4 填充缺失值 df.fillna()1.4.1 固定值填充 df.fillna(value)1.4.2 线性插值填充 df.fillna(df.interpolate()) 2 重复值处理2.1 查看重复值 df.duplicated()2.2 筛…

docker简单教程(三)常用操作

docker简单教程&#xff08;三&#xff09;常用操作 文章目录 docker简单教程&#xff08;三&#xff09;常用操作1&#xff1a;查看所有容器列表&#xff1a;docker ps -a2&#xff1a;查看正在运行的容器列表&#xff1a;docker ps3&#xff1a;运行容器&#xff1a;docker r…

测牛学堂:2023软件测试linux和shell脚本入门系列(shell的运算符)

shell中的注释 以# 开头的就是shell中的注释&#xff0c;不会被执行&#xff0c;是给编程的人看的。 shell中的运算符 shell中有很多运算符。 按照分类&#xff0c;可以分为算术运算符&#xff0c;关系运算符&#xff0c;布尔运算符&#xff0c;字符串运算符&#xff0c;文件…

把树莓派改造成无线软路由器(2)-----无线路由器模式(独立无线路由器)

本文目录 1、准备工作2、安装无线AP和管理软件3、设置网络路由3.1、树莓派的无线网络接口IP配置3.2、启用路由和IP伪装3.3、为无线网络配置DHCP和DNS服务 4、确认无线配置5、配置 AP 软件6、运行wifi无线AP 树莓派可用作网络中的一个wifi无线路由器。让使用无线接入的计算机和设…

sqlserver用SQL脚本进行备份和还原操作

--1.1备份数据库脚本 USE [master] GO BACKUP DATABASE [Test] TO DISK D:\Test\Test_20230419.bak GO --1.2还原数据库,注意一定要用NORECOVERY还原备份 USE [master] GO RESTORE DATABASE [Test] FROM DISKND:\Test\Test_20230419.bak WITH FILE 1, MOVE NTest TO ND:\Test…

后缀数组的应用:最长公共子串

题目描述 假设 str1 长度为 N N N&#xff0c;str2 长度为 M M M&#xff0c;求 str1 和 str2 的最长公共子串。 思路分析 示例&#xff1a;str1 “12abcd456”, str2 “7abcd89”&#xff0c;则str1和str2的最长公共子串为 abcd。 注意&#xff0c;子串是连续的。 动…

C. Anna, Svyatoslav and Maps(floyd + 思维)

Problem - C - Codeforces 给你一个有n个顶点的无权图&#xff0c;以及由m个顶点的序列p1,p2,...,pm给出的路径&#xff08;该路径不一定简单&#xff09;&#xff1b;对于每个1≤i<m&#xff0c;有一个弧从pi到pi1。 如果v是p的子序列&#xff0c;v1p1&#xff0c;vkpm&a…

JavaScript有几种数据类型,分别是什么?

在JavaScript中&#xff0c;我们可以分成两种类型&#xff1a;基本类型 复杂类型&#xff08;引用类型&#xff09; 两种类型的区别是&#xff1a;存储位置不同 基本类型主要为以下六种&#xff1a; Number、String、Boolean、Undefined、Null、Symbol 复杂类型/引用类型统称为…