当前位置: 代码迷 >> 综合 >> 【安卓】Media媒体库与异步查询,动态权限申请(ContentProvider,Uri)
  详细解决方案

【安卓】Media媒体库与异步查询,动态权限申请(ContentProvider,Uri)

热度:86   发布时间:2023-12-15 11:18:21.0

扉【来自第一行代码】

1. ContentProvider主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。

  1. ContentResolver中的增删改查方法都不接收表名参数,而是使用一个Uri参数代替,这个参数被称为内容URI。
    内容URI给ContentProvider中的数据建立了唯一标识符,它主要由两部分组成:authority和path。
  2. 使用parse()方法将字符串解析成对象
content://com.example.app.provider/table1
content://com.example.app.provider/table2
得到了内容URI字符串之后,我们只需要调用Uri.parse()方法,就可以将内容URI字符串解析成Uri对象了。
val uri = Uri.parse("content://com.example.app.provider/table1")

2.5 Uri补充
2.6 query的具体补充
3. 例子:读取系统联系人示例写法如下(省略了申请运行时权限部分):

class MainActivity : AppCompatActivity() {
    private val contactsList = ArrayList<String>()private lateinit var adapter: ArrayAdapter<String>override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, contactsList)contactsView.adapter = adapter……readContacts()}……private fun readContacts() {
    // 查询联系人数据contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null)?.apply {
    while (moveToNext()) {
    // 获取联系人姓名val displayName = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))// 获取联系人手机号val number = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))contactsList.add("$displayName\n$number")}adapter.notifyDataSetChanged()close()}}
}

在这里插入图片描述

2. 运行时权限:Android 6.0系统中新增了运行时权限功能。右表列出了到Android 10系统为止所有的危险权限。

  1. 一共是11组30个权限。
    在这里插入图片描述
  2. 动态权限申请
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)makeCall.setOnClickListener {
    if (ContextCompat.checkSelfPermission(this,  Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CALL_PHONE), 1)} else {
    call()}}}override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)when (requestCode) {
    1 -> {
    if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
    call()} else {
    Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show()}}}}private fun call() {
    try {
    val intent = Intent(Intent.ACTION_CALL)intent.data = Uri.parse("tel:10086")startActivity(intent)} catch (e: SecurityException) {
    e.printStackTrace()}}}

目前,使用ContentProvider是Android实现跨程序共享数据的标准方式

一:观察Device File Explorer

  1. System目录下的app目录
    在这里插入图片描述
  2. 同级System目录下的Priv-app目录
    在这里插入图片描述
  3. Priv-app目录下存在MediaProvider目录,里面有个app,功能是扫描手机中的媒体类文件
    在这里插入图片描述

二:找到储存的对应数据库位置

  1. data目录和system同级。6.0以后就看不见了,应该是没权限,之后动态获取就好了
    看到这个应该很熟晰,两层data目录下的媒体库,分别是外部和内部存储数据库内外部区别传送门,读都无需权限,写从4.4开始需权限申请
    在这里插入图片描述
  2. 导出数据库就可以看到字段结构,先来到file,我们可以找到位置,这里发现title是系统帮我们识别出来的,displayname是我们看到的文件名
    在这里插入图片描述
  3. 这个字段疑似【可做文件夹分类】文件储存的文件夹,但只到最后一个层级,要全路径还得看上面的_data【获取播放路径】
  4. 找到Audio库,发现最后一行有个这个,好像也可以作为专辑分类
  5. 具体传送门
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

三.加载音乐数据

  1. 需要拿到数据库内容就要用四大组件里的内容解析者ContentResolver,新建对象,通过该对象连接服务器,查询语言【有个MediaStore文件】
  val resolver= context?.contentResolver//cursor是查询的返回类型 前车之鉴 放到主线程卡死 加到子线程中 完成再更新UI适配 val cursor=resolver?.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,arrayOf(MediaStore.Audio.Media.DATA,MediaStore.Audio.Media.SIZE,MediaStore.Audio.Media.DISPLAY_NAME,//title可能会有编码问题MediaStore.Audio.Media.ARTIST),null,null,null)//查询感兴趣的字段 数组封装 后三个参数排序什么的 第一个参数是内容提供器//打印所有数据CursorUtil.logCursor(cursor)*/
  1. 写个遍历cursor的方法
object CursorUtil {
    fun logCursor(cursor: Cursor?){
    cursor?.let {
    //将游标复位//count是cursor含有的条目个数//columnCount是每个条目含有的字段it.moveToPosition(-1)while (it.moveToNext()){
    for (index in 0 until it.columnCount)//每个字段对应的位置[){
    println("key=${
      it.getColumnName(index)} --value=${
      it.getString(index)}")}}}}
}
  1. 运行结果,展示一条记录
    在这里插入图片描述

四.异步查询

1. 异步查询可以开启新线程,完成后再更新界面,避免堵塞进程,前车之鉴就是这个,刚打开应用一片白,而且还加载专辑图片,堵的不要不要,运气不好直接就闪退了。

在这里插入图片描述

2. 开启线程

方法一:使用Thread().start开启子线程,但此时拿到的Cursor无法在子线程中更新UI,得使用Handler回调给主线程,进行线程间通信(相对麻烦,还要解决线程问题),通过Handler进行线程间通信

     //开启一个单独的线程查询音乐数据Thread(object :Runnable{
    override fun run() {
    val cursor=resolver?.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,arrayOf(MediaStore.Audio.Media.DATA,MediaStore.Audio.Media.SIZE,MediaStore.Audio.Media.DISPLAY_NAME,MediaStore.Audio.Media.ARTIST),null,null,null)//先看下面的handler,这里通过handler的方法将查询到的cursor,通过发送消息的方式传入主线程val msg =Message.obtain()//消息池中拿消息msg.obj=cursorhandler.sendMessage(msg)}}).start()

需要一个handler对象实现方法,即使用匿名内部类

/* val handler = @SuppressLint("HandlerLeak") object : Handler(){//os包//new Handler要复写其方法,所以new一个匿名内部类override fun handleMessage(msg: Message) {//接收消息msg.let {val cursor=msg.obj as CursorCursorUtil.logCursor(cursor)}}}*/

方法二:安卓提供的继承AsyncTask类(专门用于处理异步任务)

  1. initData外创建AudioTask类继承AsyncTask,三个范型
//因为查询使用的是ContentProvider(内容提供者对应resolver),所以第一个泛型ok,第二个是执行进度/当前不需要监听进度,第三个是要求返回cursor对象。//音乐查询异步任务class AudioTask:AsyncTask<ContentResolver,Void,Cursor>(){
    //三个泛型 参数 进度 返回的Result类型//1.后台开启新线程执行任务 相当于new一个新线程,后台执行override fun doInBackground(vararg params: ContentResolver?): Cursor ?{
    //第一个参数是content 传过来的返回结果是Cursor 我们这边就一个参数拿出来就好了val cursor=params[0]?.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,arrayOf(MediaStore.Audio.Media.DATA,MediaStore.Audio.Media.SIZE,MediaStore.Audio.Media.DISPLAY_NAME,MediaStore.Audio.Media.ARTIST),null,null,null)return cursor}//2.后台任务结果回调到主线程override fun onPostExecute(result: Cursor?) {
    super.onPostExecute(result)//打印CursorCursorUtil.logCursor(result)}}
  1. initData补充,将当前resolver传过去
  val resolver= context?.contentResolverAudioTask().execute(resolver)//第二种异步传递的参数params可变参数后面取也取第一个就好了()只一个参数

方法三(推荐):安卓提供的继承AsyncQueryHandler类(专门用于处理异步数据库的操作)注释的运用

在这里插入图片描述

  1. 为了复写其方法,使用该类时新建匿名内部类,主要八个方法,分别是增删改查及其回调,好处是增删改查在子线程中,回调方法在子线程中
  2. 声明handler为静态类,防止内存泄漏,回调的Cursor自动参数
  val resolver= context?.contentResolverval handler= @SuppressLint("HandlerLeak") object:AsyncQueryHandler(resolver){
    //token是用于区分的 调用查询可以加入token 同flagoverride fun onQueryComplete(token: Int, cookie: Any?, cursor: Cursor?) {
    //查询完成的回调 只有回调是在主线程中的//打印数据CursorUtil.logCursor(cursor)/* when(token){条目或者非常多 内嵌语句非常多 就可以考虑用cookie传递adapter startquery中第二个参数,现在没适配adapter先写null0->adapter11->adapter2}*/}}//开始查询 如果与n个startquery要加识别token,第二个null是cookie,可以传adapter等想要的handler.startQuery(0,null,MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,arrayOf(MediaStore.Audio.Media.DATA,MediaStore.Audio.Media.SIZE,MediaStore.Audio.Media.DISPLAY_NAME,MediaStore.Audio.Media.ARTIST),null,null,null)

在这里插入图片描述
start方法对应on方法获取结果

补充:

  1. 同一首歌曲在不同手机中分配的MUSIC_ID是不同的
    在这里插入图片描述
    在这里插入图片描述
   val resolver= this?.contentResolver//cursor是查询的返回类型 前车之鉴 放到主线程卡死 加到子线程中 完成再更新UI适配val cursor=resolver?.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,arrayOf(MediaStore.Audio.Media.TITLE ),MediaStore.Audio.Media.TITLE + "=?", arrayOf("安静"), null)//查询感兴趣的字段 数组封装 后三个参数排序什么的 第一个参数是内容提供器
/* val cursor = contentResolver.query(ContactsContract.Contacts.CONTENT_URI,arrayOf(ContactsContract.Contacts.DISPLAY_NAME),ContactsContract.Contacts.DISPLAY_NAME + "=?", arrayOf("张三"), null)*///打印所有数据CursorUtil.logCursor(cursor)

在这里插入图片描述

  相关解决方案