Android内容提供者contentResolver

近期回顾

最近工作都是处理设备信息相关的数据,所以就多接触了一下BuildgetSystemService()contentResolver相关的东西,在这里做一个小小的总结,整理下近期自己工作上的收获。

Build

Build是系统相关配置的东西,比如,部分手机对应的一部分Build的值如下:

PhoneMODELDEVICEBRAND
Honor 9 LiteLLD-AL00HWLLD-HHONOR
XIAOMI MIX 2MIX 2chironXiaomi
Redmi K40 GamingM2012K10CaresRedmi

在一般情况下,设备的这些属性都是只读的,是设备制造商在构建Android系统时定义在系统镜像中,在/system/build.prop/或者/system/etc/prop.default文件里可以查看。当然,不能通过这些值来判断设备是否安全和设备是否唯一–特殊情况下,这些值都是可以修改的。比如,在root的设备上或者使用了xposed框架等工具可以修改这些属性的值。

直接修改对应的属性的值,或者说,不正确的修改其值容易导致系统不稳定或者损坏。相反,使用xposed框架工具修改值反而会更加安全和灵活。而Xposed相关比较有名的,有Magisk、Lposed以及太极等,加上对应的工具,可以实现的功能实在太多了。

尝试过一段时间的Magisk和Lposed,发现能用实现的功能,在于你的想象力(夸张点,但确实)。比如,Lposed有一个模块,实现了IOS音效,有手工耿的感觉了。还有WX、Tiktok、QQ等相关的模块。不过这些大厂的APP都有对Xposed框架作检测和反制的措施,矛和盾之间的较量。

话题扯远了,回到设备信息这一块,除了Build可以获取到设备相关信息,getSystemService()也可以获取到部分的设备信息。

getSystemService

如果说Build获取的设备信息是固定的,那么有getSystemService获取的设备相关信息就是可变的。

//TelephonyManager获取当前使用的SIM卡上的部分信息
getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
//SensorManager获取设备上传感器相关的数据
getSystemService(Context.SENSOR_SERVICE) as SensorManager
//CameraManager用于访问和控制设备上的可用摄像头
getSystemService(Context.CAMERA_SERVICE) as CameraManager
//LocationManager用于管理设备的位置服务,可以用来获取设备的地理位置信息,包括经纬度坐标,海拔高度、速度等
getSystemService(Context.LOCATION_SERVICE) as LocationManager
//WindowManager用于管理Window和Layout,可以用来创建、更新、添加、移除Window等操作
getSystemService(Context.WINDOW_SERVICE) as WindowManager
//WifiManager管理Wi-Fi相关的服务,提供了一系列方法来管理设备的 Wi-Fi 功能,包括打开和关闭 Wi-Fi、扫描可用的 Wi-Fi 网络、连接到指定的 Wi-Fi 网络、获取当前连接的 Wi-Fi 状态等
getSystemService(Context.WIFI_SERVICE) as WifiManager
//BatteryManager用于获取设备电池信息,提供了一系列方法和常量来获取设备当前的电池状态和电池信息。
getSystemService(Context.BATTERY_SERVICE) as BatteryManager
//BluetoothManager用于管理蓝牙连接和操作,提供了一系列方法和常量来控制设备的蓝牙功能,包括打开和关闭蓝牙、扫描可用的蓝牙设备、连接到指定的蓝牙设备等。
getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
//ActivityManager这个类的功能就多了,可以用来管理和监控应用程序的活动状态、任务栈、进程等
getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager

比如上面的,就是其中一些比较常用的系统服务,通过对应的Manager可以获取到对应的设备信息。

TelephonyManager可以获取到SIM卡的状态、SIM的手机号码、归属国家(SIM提供商的国家代码)、SIM的序列号等信息,当然前提是需要android.permission.READ_PHONE_STATUS电话状态权限。

CameraManager可以获取到相机相关的信息,这只是访问信息,如果想要控制相机的话,就需要android.permission.CAMERA权限。

还有WindowManager,这个应该非常熟悉了,对Window窗口进行操作,最简单的就是利用WindowManager.LayoutParams控制dialog的位置,大小和透明度。

优缺点

Build相比,getSystemService的使用会有一些繁琐,部分的服务甚至需要权限才能访问,甚至,如TelephonyManager获取SIM卡信息,在获取到权限的基础上,还需要判断一下设备是否拥有对应的SIM卡。虽然getSystemService信息也是可以被修改的,但是相对于Build,它的可靠性更高,在设备安全方面更具有优势。

contentResolver

终于谈到这个了。contentResolver身为四大组件之一,其重要程度不明而谕。

  1. Activity作为UI呈现的载体,用于与用户的交互。
  2. Service作为后台运行的组件,用于执行长时间运行的任务,一般用来执行播放音乐,文件下载和处理数据等任务。
  3. BoradcastReceiver广播主要用来接收系统广播或应用内广播。比如,如果要监听设备的音量变化,就可以注册VolumeReceiver来获取音量的变化情况。
  4. ContentResolver是Android中用于数据提供者交互的重要类之一,使应用程序能够执行各种数据操作,并且能够以安全和有效的方式去处理数据。

与contentResolver的接触

上面第四点介绍contentResolver有点抽象,我用这段时间自己接触的contentResolver来解释吧。

设备上的短信、通讯录、通话记录之类的信息,都存储在设备的数据库中的,想要处理这些数据,就需要使用contentResolver

获取通话记录

举一个获取通话记录的例子吧。 下面贴上三段代码

// 获取 ContentResolver
val contentResolver = contentResolver

// 定义要获取的通话记录数据字段
val projection = arrayOf(
    CallLog.Calls.NUMBER,
    CallLog.Calls.DATE,
    CallLog.Calls.DURATION,
    CallLog.Calls.TYPE,
    CallLog.Calls.CACHED_NAME
)

val limit = 2000
val totalCalls = getCallLogCount()
val queryLimit = if (totalCalls <= limit) totalCalls else limit

// 执行查询
val cursor: Cursor? = contentResolver.query(
    CallLog.Calls.CONTENT_URI,
    projection,
    null,
    null,
    "${CallLog.Calls.DATE} DESC"
)
val callRecords = mutableListOf<CallRecord>()
cursor?.use { c ->
    // 获取列索引
    val columnIndexNumber = c.getColumnIndex(CallLog.Calls.NUMBER)
    val columnIndexType = c.getColumnIndex(CallLog.Calls.TYPE)
    val columnIndexDuration = c.getColumnIndex(CallLog.Calls.DURATION)
    val columnIndexDate = c.getColumnIndex(CallLog.Calls.DATE)
    val columnIndexCName = c.getColumnIndex(CallLog.Calls.CACHED_NAME)

    // 确保列索引有效
    if (columnIndexNumber != -1 && columnIndexType != -1 &&
        columnIndexDuration != -1 && columnIndexDate != -1 && columnIndexCName != -1
    ) {
        // 创建 Map 来统计每条通话记录的通话次数
        var count = 0
        // 遍历查询结果
        while (c.moveToNext() && count < queryLimit) {
            val number = c.getString(columnIndexNumber)
            val date = c.getLong(columnIndexDate)
            val duration = c.getLong(columnIndexDuration)
            val type = c.getInt(columnIndexType)
            val name = c.getString(columnIndexCName) ?: ""
            val callRecord = CallRecord(number, type, date, duration, name)
            callRecords.add(callRecord)
            count++
        }
    }
}

这串代码主要的功能就是获取最新的2000条通话记录信息,其中,callRecords数组里就是获取到的所有数据。getCallLogCount()方法和CallRecord类代码贴在下面。

private fun getCallLogCount(): Int {
    val cursor = contentResolver.query(
        CallLog.Calls.CONTENT_URI,
        arrayOf(CallLog.Calls._ID),
        null,
        null,
        null
    )
    val count = cursor?.count ?: 0
    cursor?.close()
    return count
}

获取通话记录的数量,这里做了一个限制,防止通话记录数据过大。假如通话记录数据不满2000条,则获取全部,如果超过2000条,则只获取最新的2000条通话记录。

data class CallRecord(
    val phoneNumber: String,
    val type: Int, // CallLog.Calls.TYPE
    val date: Long,
    val duration: Long,
    val name: String
)

如此,根据上面的代码,就可以获取到通话记录的类型、号码、时间、时长和名字。其中,时长单位是秒,名字则从通讯录中获取,假如通讯录中没有此人,那么该值有可能为null。而type,通话类型,对应的通话类型如下:

对应的含义
1呼入的通话记录
2呼出的通话记录
3未接的通话记录
4语音信箱的通话记录
5被拒绝的通话记录
6被阻止的通话记录
7外部应用程序接听的通话记录,被当前设备以外的设备接听

pass:当type==3的时候,理论上是没有通话时长的,但有的品牌的手机,在号码拨出去的时候就开始计算通话时长了。

当然,如果直接使用上面的代码,还是会报错的,因为,没有对应的权限。 需要先在AndroidManifest中添加权限

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

在代码中动态申请,申请通过后才可以获取通话记录信息。

ps: 通话记录是敏感权限,不仅需要在AndroidManifest中静态请求,还要在代码中动态请求。如果只有静态请求而没有动态请求,在Android6.0以上的设备上就会报错。

在AndroidManifest中静态请求了敏感权限,在设置界面中的应用详情界面下的权限子界面就会有对应的权限显示。当然,像网络、wifi权限这种属于静态权限,不敏感,只需要在AndroidManifest中请求就够了。

随着Android的发展,权限也会越来越严格。

获取通讯录信息

获取短信、获取通讯录、获取通话记录之类的,都是通过contentResolver获取存储在手机上的数据的。

获取全部通讯录信息

其代码如下:

// 执行查询
val cursor = contentResolver.query(
    ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
    null,
    null,
    null,
    null
)
cursor?.use { c ->
    // 遍历查询结果
    while (c.moveToNext()) {
        try {
            //账号类型
            val accountType =
                c.getString(c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.ACCOUNT_TYPE_AND_DATA_SET))
            //是否星标
            val starred =
                c.getInt(c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.STARRED))
            //上次更新时间
            val contactLastUpdatedTimestamp =
                c.getLong(c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_LAST_UPDATED_TIMESTAMP))
            //通话记录中的总次数
            val timeUsed =
                c.getInt(c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TIMES_USED))
            //contact id
            val contactId =
                c.getLong(c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_ID))
            //反映此联系人Groups#GROUP_VISIBLE的任何状态的 标志
            val inVisibleGroup =
                c.getInt(c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.IN_VISIBLE_GROUP))
            //用作联系人显示名称的数据类型,例如结构化名称或电子邮件地址。
            val displayNameSource =
                c.getInt(c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME_SOURCE))
            //号码
            val mobile =
                c.getString(c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
                    ?: ""
            //名字
            val name =
                c.getString(c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
                    ?: ""
            //用户头像id
            val photoId =
                c.getLong(c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.PHOTO_ID))
            //手机mime类型
            val mimeType =
                c.getString(c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.MIMETYPE))
                    ?: ""
        } catch (e: Exception) {
            println(e.message)
        }
    }
}

获取权限后,contentResolver访问ContactsContract.CommonDataKinds.Phone.CONTENT_URI获取对应数据库的信息,拿到通讯录数据。

这里有两点需要注意的,

  1. 通讯录表里没有创建时间,只有更新时间,假如创建数据后没有更改,那么其就是更新时间,如果修改了,那么更新时间也跟着修改。
  2. 单条手机号对应着一条通讯录信息,意味着,同一个名字下面如果有两个电话号码的话,那将会有两条通讯录数据。
单条通讯录

既然通讯录的访问获取和短信是通话记录是一致的,那,为什么还要在这里重复贴一遍代码呢?

那,再贴一段代码:

    @SuppressLint("Range")
    private val contactLauncher =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            if (it.resultCode != RESULT_OK) return@registerForActivityResult
            val uri: Uri? = it.data?.data
            val cr = this.contentResolver
            uri?.let {
                val cursor = cr.query(it, null, null, null, null)
                cursor?.let { cursorItem ->
                    try {
                        if (cursorItem.moveToFirst()) {
//                            val name =
//                                cursorItem.getString(cursorItem.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME))
//                            val phone =
//                                cursorItem.getString(cursorItem.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
//                                    .toString().replace(" ", "")
//                            cursorItem.close()
//                            tvContent?.text = "$name\t$phone"
                            val creationTime =
                                cursorItem.getLong(cursorItem.getColumnIndex(ContactsContract.RawContacts.LAST_TIME_CONTACTED))//联系总次数
                            val accountType =
                                cursorItem.getString(cursorItem.getColumnIndex(ContactsContract.CommonDataKinds.Phone.ACCOUNT_TYPE_AND_DATA_SET))//账号类型
                            val starred =
                                cursorItem.getInt(cursorItem.getColumnIndex(ContactsContract.CommonDataKinds.Phone.STARRED))//是否星标
                            val contactLastUpdatedTimestamp =
                                cursorItem.getLong(cursorItem.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_LAST_UPDATED_TIMESTAMP))//上次更新时间
                            val timeUsed =
                                cursorItem.getInt(cursorItem.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TIMES_USED))//通话记录中的总次数
                            val contactId =
                                cursorItem.getLong(cursorItem.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_ID))//contact id
                            val inVisibleGroup =
                                cursorItem.getInt(cursorItem.getColumnIndex(ContactsContract.CommonDataKinds.Phone.IN_VISIBLE_GROUP))//反映此联系人Groups#GROUP_VISIBLE的任何状态的 标志
                            val displayNameSource =
                                cursorItem.getInt(cursorItem.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME_SOURCE))//用作联系人显示名称的数据类型,例如结构化名称或电子邮件地址。
                            val mobile =
                                cursorItem.getString(cursorItem.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))//号码
                                    ?: ""
                            val name =
                                cursorItem.getString(cursorItem.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))//名字
                                    ?: ""
                            val photoId =
                                cursorItem.getLong(cursorItem.getColumnIndex(ContactsContract.CommonDataKinds.Phone.PHOTO_ID))//用户头像id
                            val mimeType =
                                cursorItem.getString(cursorItem.getColumnIndex(ContactsContract.CommonDataKinds.Phone.MIMETYPE))//手机mime类型
                                    ?: ""
                            tvContent?.text =
                                "$creationTime,$accountType,$starred.$contactLastUpdatedTimestamp,$timeUsed,$contactId,$inVisibleGroup,$displayNameSource,$mobile,$name,$photoId,,$mimeType"
                        }
                    } catch (_: Exception) {
                    }
                }
            }
        }

val intent = Intent(
    Intent.ACTION_PICK,
    ContactsContract.CommonDataKinds.Phone.CONTENT_URI
)
contactLauncher.launch(intent)

这段代码的功能是打开通讯录,点击获取单条通讯录信息。

和获取全部通讯录数据不同的是,获取单条通讯录信息需要用户手动操作选择通讯录数据。

相同的是,同样是使用contentResolver访问ContactsContract.CommonDataKinds.Phone.CONTENT_URI

那,有什么区别吗?

把AndroidManifest中<uses-permission android:name="android.permission.READ_CONTACTS" />权限注释或者删除了,然后去除代码中的动态权限请求,再去运行上面获取单条通讯录信息的代码。

神奇的事情发生了,即便不需要权限,也是能获取到单条通讯录信息的。

是的,不需要READ_CONTACTS"权限,不需要READ_CONTACTS"权限,不需要READ_CONTACTS"权限。

虽然没法获取所有的通讯录信息,但通过这种方式,可以直接且不申请权限获取单条通讯录信息。

这,又有什么用呢?

Google Play Store应用市场上架应用是有要求的,某些分类的App,是不能申请某些权限的,不然无法通过审核。而通过这种方式,可以减少权限的申请,提高App审核的通过率。当然,这个方法放在国内也是有效的。

个人想来,这个应该是“系统特性”。

结论

本篇文章虽然标题是contentResolver,但只是本人最近一段时间工作中的一些学习和发现。

在四大组件中,最熟悉的还是ActivityBroadcastReceiverServicecontentResolver用的少,尤其是Service,没有接触过音乐播放器类的App,能使用Service,但不精通,工作中使用的场景太少了。

这几个月下来对contentResolver的使用(ps:当然也不全是在做contentResolver有关的东西,工作如果只有这些个,太无趣了),让我对Android这个系统有了一些新的了解。

其他工作

除此之外,还有apk包体积优化,数据埋点方面的工作。

尤其是apk包体积优化,看着原本20M+体积的包,在自己努力下只有11M+大小的时候,那个成就感(ps:虽然只是照着文章步骤一步一步来的)。只是可惜的是,原本的目标是10M以下的,整理了一遍后发现,11M是极限了(听友商说,他们能压到10M,同样功能的实现,不知道他们使用了什么方法)。

数据埋点则全是重复性的工作,只是在room那边卡了一下。


最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题
图片

  • 13
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值