Procházet zdrojové kódy

MediaStore 增加分页查询

liuxiaolong před 2 roky
rodič
revize
57f5121058

+ 162 - 168
app/src/main/java/com/convenient/android/lib/ui/sample/media/MediaFilesPage.kt

@@ -4,7 +4,6 @@ import android.Manifest
 import android.app.Activity
 import android.content.Intent
 import android.content.pm.PackageManager
-import android.net.Uri
 import android.os.Build
 import android.os.Environment
 import android.provider.Settings
@@ -13,14 +12,20 @@ import androidx.activity.result.contract.ActivityResultContracts
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.*
+import androidx.compose.material.Icon
+import androidx.compose.material.RadioButton
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Delete
-import androidx.compose.material3.ElevatedButton
+import androidx.compose.material3.*
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.*
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -41,7 +46,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel
 import com.convenient.android.common.extension.lengthToFitMemorySize
 import com.convenient.android.common.extension.spSave
 import com.convenient.android.common.extension.toFile
-import com.convenient.android.common.utils.ToastUtil
+import com.convenient.android.common.media.config.MediaSortOrder
+import com.convenient.android.common.media.config.MediaSortType
 import com.convenient.android.common.utils.date.DateUtils
 import com.convenient.android.lib.R
 import com.convenient.android.lib.ui.theme.SampleTheme
@@ -54,16 +60,15 @@ import com.convenient.android.lib.ui.theme.SampleTheme
  */
 
 @Composable
-fun MediaPage(){
+fun MediaPage() {
     val context = LocalContext.current
-
     var rememberPermission by remember {
         mutableStateOf(ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
     }
-    if (rememberPermission){
+    if (rememberPermission) {
         MediaFilesPage()
-    }else{
-        PermissionPage{
+    } else {
+        PermissionPage {
             rememberPermission = true
         }
     }
@@ -72,132 +77,51 @@ fun MediaPage(){
 
 @Composable
 fun MediaFilesPage() {
-
     val viewModel: MediaFilesViewModel = viewModel()
+    val result = viewModel.result.collectAsState()
 
-    val context = LocalContext.current
-
-    val chooseDirLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult(), onResult = {
-        it.data?.data?.let {
-            val path = resolveContentUri(context, it)
-            context.spSave("dir", path)
-            viewModel.changeQueryDir(path)
-        }
-    })
-
-    Column(
-        modifier = Modifier
-            .fillMaxWidth()
-            .padding(horizontal = 16.dp)
-            .verticalScroll(rememberScrollState())
-    ) {
-
-        ConstraintLayout(
-            modifier = Modifier
-                .fillMaxWidth()
-        ) {
-            val (btnFromFiles, btnFromMediaStore, btnChooseDir) = createRefs()
-
-            ElevatedButton(
-                onClick = { chooseDirLauncher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)) },
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .constrainAs(btnChooseDir) {
-                        top.linkTo(parent.top)
-                        start.linkTo(parent.start)
-                        end.linkTo(parent.end)
-                    }) {
-                Text(text = "选择文件夹")
-            }
-
-            ElevatedButton(onClick = { viewModel.changeQueryType(MediaQueryType.FILES) }, modifier = Modifier
-                .padding(end = 8.dp)
-                .constrainAs(btnFromFiles) {
-                    top.linkTo(btnChooseDir.bottom)
-                    start.linkTo(parent.start)
-                    end.linkTo(btnFromMediaStore.start)
-                    width = Dimension.fillToConstraints
-                }) {
-                Text(text = "从FilesMedia获取")
-
-            }
-
-            ElevatedButton(onClick = { viewModel.changeQueryType(MediaQueryType.MEDIA_STORE) },
-                modifier = Modifier
-                    .padding(start = 8.dp)
-                    .constrainAs(btnFromMediaStore) {
-                        top.linkTo(btnFromFiles.top)
-                        start.linkTo(btnFromFiles.end)
-                        end.linkTo(parent.end)
-                        width = Dimension.fillToConstraints
-                    }) {
-                Text(text = "从MediaStore获取")
-            }
-        }
-
-        QueryInfoPage(viewModel = viewModel)
-        Spacer(
-            modifier = Modifier
-                .fillMaxWidth()
-                .height(1.dp)
-                .background(color = Color.LightGray)
-        )
-        ResultInfoPage(viewModel = viewModel)
-
-    }
-}
+    LazyColumn {
 
-@Composable
-private fun PermissionPage(permissionCallback : ()-> Unit) {
-
-    val context = LocalContext.current
-    val androidRStoragePermissionLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult(), onResult = {
-        if (it.resultCode == Activity.RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
-            if (Environment.isExternalStorageManager()) {
-                ToastUtil.showToast(context, "访问所有文件权限获取成功")
-                permissionCallback.invoke()
-            }
+        item {
+            QueryInfoPage(viewModel = viewModel)
         }
-    })
-
-    val permissionLaunch = rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission(), onResult = {
-        if (it) {
-            ToastUtil.showToast(context, "存储权限获取成功")
-            permissionCallback.invoke()
 
+        item {
+            ResultInfoPage(viewModel = viewModel)
         }
-    })
 
-    Column(modifier = Modifier
-        .fillMaxWidth()
-        .fillMaxHeight(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
-
-        ElevatedButton(onClick = {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
-                if (Environment.isExternalStorageManager().not()) {
-                    androidRStoragePermissionLauncher.launch(Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:${context.packageName}")))
-                } else {
-                    if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED){
-                        ToastUtil.showToast(context, "已获取存储权限")
-                    }else{
-                        permissionLaunch.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+        items(result.value){item->
+            Row(
+                modifier = Modifier
+                    .padding(vertical = 8.dp)
+                    .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically
+            ) {
+                Image(
+                    imageVector = ImageVector.vectorResource(id = if (item.isFile) R.drawable.ic_icons8_file else R.drawable.ic_icons8_folder),
+                    modifier = Modifier.padding(top = 8.dp, bottom = 8.dp),
+                    contentDescription = null
+                )
+
+                Column {
+                    Text(text = item.name, style = TextStyle(color = Color.Black, fontWeight = FontWeight.Bold, fontSize = 14.sp))
+                    Row {
+                        Text(text = DateUtils.getFormatDate(item.lastModified), fontSize = 12.sp)
+                        Spacer(modifier = Modifier.padding(horizontal = 8.dp))
+                        Text(text = item.mediaPath.toFile()?.lengthToFitMemorySize() ?: "", fontSize = 12.sp)
                     }
                 }
-            } else {
-                permissionLaunch.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
             }
-        }) {
-            Text(text = "获取存储权限")
         }
     }
-
 }
 
 
 @Composable
 fun QueryInfoPage(viewModel: MediaFilesViewModel) {
-    val config = viewModel.config.collectAsState()
     val context = LocalContext.current
+    val config = viewModel.config.collectAsState()
+    val dir = viewModel.dir.collectAsState()
+    val queryType = viewModel.queryType.collectAsState()
 
     val addIgnoreFolderLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult(), onResult = {
         it.data?.data?.let {
@@ -214,13 +138,62 @@ fun QueryInfoPage(viewModel: MediaFilesViewModel) {
         }
     })
 
-    val dir = viewModel.dir.collectAsState()
-    val queryType = viewModel.queryType.collectAsState()
+    val chooseDirLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult(), onResult = {
+        it.data?.data?.let {
+            val path = resolveContentUri(context, it)
+            context.spSave("dir", path)
+            viewModel.changeQueryDir(path)
+        }
+    })
+
+    ConstraintLayout(
+        modifier = Modifier
+            .fillMaxWidth()
+    ) {
+        val (btnFromFiles, btnFromMediaStore, btnChooseDir) = createRefs()
+
+        ElevatedButton(
+            onClick = { chooseDirLauncher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)) },
+            modifier = Modifier
+                .fillMaxWidth()
+                .constrainAs(btnChooseDir) {
+                    top.linkTo(parent.top)
+                    start.linkTo(parent.start)
+                    end.linkTo(parent.end)
+                }) {
+            Text(text = "选择文件夹")
+        }
+
+        ElevatedButton(onClick = { viewModel.changeQueryType(MediaQueryType.FILES) }, modifier = Modifier
+            .padding(end = 8.dp)
+            .constrainAs(btnFromFiles) {
+                top.linkTo(btnChooseDir.bottom)
+                start.linkTo(parent.start)
+                end.linkTo(btnFromMediaStore.start)
+                width = Dimension.fillToConstraints
+            }) {
+            Text(text = "从FilesMedia获取")
+
+        }
+
+        ElevatedButton(onClick = { viewModel.changeQueryType(MediaQueryType.MEDIA_STORE) },
+            modifier = Modifier
+                .padding(start = 8.dp)
+                .constrainAs(btnFromMediaStore) {
+                    top.linkTo(btnFromFiles.top)
+                    start.linkTo(btnFromFiles.end)
+                    end.linkTo(parent.end)
+                    width = Dimension.fillToConstraints
+                }) {
+            Text(text = "从MediaStore获取")
+        }
+    }
 
-    InfoItem(title = "目录:", info = dir.value, infoPosition = InfoPosition.END)
-    InfoItem(title = "获取方式:", info = queryType.value.name, infoPosition = InfoPosition.END)
 
     Column() {
+        InfoItem(title = "目录:", info = dir.value, infoPosition = InfoPosition.END)
+        InfoItem(title = "获取方式:", info = queryType.value.name, infoPosition = InfoPosition.END)
+
         InfoItem(title = "查询的文件格式:", info = config.value.supportMimeTypes.toString(), infoPosition = InfoPosition.END)
         TextField(
             keyboardOptions = KeyboardOptions(autoCorrect = false, capitalization = KeyboardCapitalization.None),
@@ -234,27 +207,25 @@ fun QueryInfoPage(viewModel: MediaFilesViewModel) {
             }, placeholder = {
                 Text(text = "png,jpg 以,分割")
             })
-    }
-
-    Row {
-        val includeFolder = config.value.includeFolder
-        RadioButton(selected = includeFolder, onClick = {
-            viewModel.changeConfigIncludeFolder(includeFolder.not())
-        })
-        InfoItem(title = "结果是否包含文件夹:", info = config.value.includeFolder.toString())
-
-    }
-
-    Row {
-        val recursively = config.value.recursively
-        RadioButton(selected = recursively, onClick = {
-            viewModel.changeConfig(config.value.copy(recursively = recursively.not()))
-        })
-        InfoItem(title = "遍历子文件夹:", info = config.value.recursively.toString())
-
-    }
+        Row {
+            val includeFolder = config.value.includeFolder
+            RadioButton(selected = includeFolder, onClick = {
+                viewModel.changeConfigIncludeFolder(includeFolder.not())
+            })
+            InfoItem(title = "结果是否包含文件夹:", info = config.value.includeFolder.toString())
+        }
+        Row {
+            val recursively = config.value.recursively
+            RadioButton(selected = recursively, onClick = {
+                viewModel.changeConfig(config.value.copy(recursively = recursively.not()))
+            })
+            InfoItem(title = "遍历子文件夹:", info = config.value.recursively.toString())
+        }
+        InfoItem(title = "排序类型:", info = "")
+        SortTypeButton(viewModel = viewModel)
+        InfoItem(title = "排序方式:", info = "")
+        OrderTypeButton(viewModel = viewModel)
 
-    Column {
         InfoItem(title = "忽略的文件:", info = "")
         Row {
             ElevatedButton(onClick = { addIgnoreFolderLauncher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)) }) {
@@ -275,6 +246,55 @@ fun QueryInfoPage(viewModel: MediaFilesViewModel) {
                 }
             }
         }
+        Spacer(
+            modifier = Modifier
+                .fillMaxWidth()
+                .height(1.dp)
+                .background(color = Color.LightGray)
+        )
+
+    }
+}
+
+@Composable
+fun SortTypeButton(viewModel: MediaFilesViewModel) {
+    val sortType = viewModel.config.collectAsState()
+
+    val types = listOf(MediaSortType.DATE, MediaSortType.SIZE, MediaSortType.NAME)
+
+    Row {
+        types.forEach {
+            FilledTonalButton(
+                colors = ButtonDefaults.filledTonalButtonColors(
+                    containerColor = if (it == sortType.value.sortType) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary.copy(0.1F)
+                )
+                ,onClick = {
+                viewModel.changeSortType(it)
+            }) {
+                Text(text = it.name, color = Color.White)
+            }
+        }
+    }
+}
+
+@Composable
+fun OrderTypeButton(viewModel: MediaFilesViewModel) {
+    val orderType = viewModel.config.collectAsState()
+
+    val types = listOf(MediaSortOrder.ASC, MediaSortOrder.DESC)
+
+    Row {
+        types.forEach {
+            FilledTonalButton(
+                colors = ButtonDefaults.filledTonalButtonColors(
+                    containerColor = if (it == orderType.value.order) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary.copy(0.1F)
+                )
+                ,onClick = {
+                    viewModel.changeOrder(it)
+                }) {
+                Text(text = it.name, color = Color.White)
+            }
+        }
     }
 }
 
@@ -329,32 +349,6 @@ fun ResultInfoPage(viewModel: MediaFilesViewModel) {
     InfoItem(title = "数量", info = count.toString())
     InfoItem(title = "结果文件格式", info = mimeTypes.toString())
 
-    result.value.forEach { item ->
-
-        Row(
-            modifier = Modifier
-                .padding(vertical = 8.dp)
-                .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically
-        ) {
-
-            Image(
-                imageVector = ImageVector.vectorResource(id = if (item.isFile) R.drawable.ic_icons8_file else R.drawable.ic_icons8_folder),
-                modifier = Modifier.padding(top = 8.dp, bottom = 8.dp),
-                contentDescription = null
-            )
-
-            Column {
-                Text(text = item.name, style = TextStyle(color = Color.Black, fontWeight = FontWeight.Bold, fontSize = 14.sp))
-                Row {
-                    Text(text = DateUtils.getFormatDate(item.lastModified), fontSize = 12.sp)
-                    Text(text = item.mediaPath.toFile()?.lengthToFitMemorySize() ?: "", fontSize = 12.sp)
-                }
-            }
-
-        }
-    }
-
-
 }
 
 

+ 1 - 0
app/src/main/java/com/convenient/android/lib/ui/sample/media/MediaFilesRepository.kt

@@ -1,5 +1,6 @@
 package com.convenient.android.lib.ui.sample.media
 
+import com.convenient.android.common.extension.withIO
 import com.convenient.android.common.media.MediaBean
 import com.convenient.android.common.media.config.MediaQueryConfig
 import com.convenient.android.common.media.scan.FileStore

+ 14 - 0
app/src/main/java/com/convenient/android/lib/ui/sample/media/MediaFilesViewModel.kt

@@ -1,11 +1,14 @@
 package com.convenient.android.lib.ui.sample.media
 
 import android.app.Application
+import android.util.Log
 import androidx.lifecycle.AndroidViewModel
 import androidx.lifecycle.viewModelScope
 import com.convenient.android.common.extension.spGetString
 import com.convenient.android.common.media.MediaBean
 import com.convenient.android.common.media.config.MediaQueryConfig
+import com.convenient.android.common.media.config.MediaSortOrder
+import com.convenient.android.common.media.config.MediaSortType
 import com.convenient.android.common.utils.string.SharedPreferencesSave
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -73,6 +76,17 @@ class MediaFilesViewModel(application: Application) : AndroidViewModel(applicati
         })
     }
 
+    fun changeSortType(sortType: MediaSortType){
+        changeConfig(copyNewConfig {
+            this.sortType = sortType
+        })
+    }
+    fun changeOrder(order: MediaSortOrder){
+        changeConfig(copyNewConfig {
+            this.order = order
+        })
+    }
+
 
     private fun copyNewConfig(config: MediaQueryConfig.()-> Unit) : MediaQueryConfig{
         return _config.value.copy().also(config)

+ 79 - 0
app/src/main/java/com/convenient/android/lib/ui/sample/media/MediaPermissionPage.kt

@@ -0,0 +1,79 @@
+package com.convenient.android.lib.ui.sample.media
+
+import android.Manifest
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Environment
+import android.provider.Settings
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.Text
+import androidx.compose.material3.ElevatedButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.core.content.ContextCompat
+import com.convenient.android.common.utils.ToastUtil
+
+/**
+ * @classname:
+ * @author: LiuXiaoLong
+ * @date: 2022/8/30
+ * description:
+ */
+
+
+
+@Composable
+fun PermissionPage(permissionCallback: () -> Unit) {
+
+    val context = LocalContext.current
+    val androidRStoragePermissionLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult(), onResult = {
+        if (it.resultCode == Activity.RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            if (Environment.isExternalStorageManager()) {
+                ToastUtil.showToast(context, "访问所有文件权限获取成功")
+                permissionCallback.invoke()
+            }
+        }
+    })
+
+    val permissionLaunch = rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission(), onResult = {
+        if (it) {
+            ToastUtil.showToast(context, "存储权限获取成功")
+            permissionCallback.invoke()
+        }
+    })
+
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .fillMaxHeight(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally
+    ) {
+
+        ElevatedButton(onClick = {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                if (Environment.isExternalStorageManager().not()) {
+                    androidRStoragePermissionLauncher.launch(Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION))
+                } else {
+                    if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
+                        ToastUtil.showToast(context, "已获取存储权限")
+                    } else {
+                        permissionLaunch.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+                    }
+                }
+            } else {
+                permissionLaunch.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+            }
+        }) {
+            Text(text = "获取存储权限")
+        }
+    }
+
+}

+ 3 - 1
lib_common/README.md

@@ -1,3 +1,5 @@
 # lib_common
 
-项目基础组件
+## 项目基础组件
+
+### [媒体文件扫描](./媒体文件扫描文档.md)

+ 1 - 1
lib_common/src/main/java/com/convenient/android/common/extension/ListExtensions.kt

@@ -14,5 +14,5 @@ import com.convenient.android.common.media.config.SortUtils
  * 排序扩展方法
  */
 fun <T, R : Comparable<R>> List<T>.sort(order: MediaSortOrder, selector: (T) -> R?) : List<T>{
-    return SortUtils.sort(order, data = this, selector = selector)
+    return SortUtils.sort(order, data = this, selector = selector).toList()
 }

+ 2 - 2
lib_common/src/main/java/com/convenient/android/common/media/basic/Query.kt

@@ -12,9 +12,9 @@ import com.convenient.android.common.media.config.MediaQueryConfig
 interface Query {
 
 
-    fun query(dir : String, config : MediaQueryConfig.() -> Unit) : List<MediaBean>
+    suspend fun query(dir : String, config : MediaQueryConfig.() -> Unit) : List<MediaBean>
 
 
-    fun query(dir: String, config: MediaQueryConfig) : List<MediaBean>
+    suspend fun query(dir: String, config: MediaQueryConfig) : List<MediaBean>
 
 }

+ 32 - 10
lib_common/src/main/java/com/convenient/android/common/media/config/MediaQueryConfig.kt

@@ -10,11 +10,11 @@ import java.io.File
  */
 data class MediaQueryConfig(
     /**
-     * 排序方式
+     * 升序降序
      */
     var order: MediaSortOrder = MediaSortOrder.DESC,
     /**
-     * 排序类型
+     * 排序方式
      */
     var sortType : MediaSortType = MediaSortType.DATE,
     /**
@@ -22,6 +22,14 @@ data class MediaQueryConfig(
      * 不管是文件还是文件夹,只要向匹配就忽略,
      */
     var ignoreChildFiles: List<File> = mutableListOf(),
+
+    /**
+     * 支持查询的文件格式
+     * 根据文件名称后缀 进行判断 例如:png jpg
+     * 都使用小写
+     */
+    var supportMimeTypes: List<String> = mutableListOf(),
+
     /**
      * 返回的结果中中是否包含文件夹
      * 仅在FileMedia中生效
@@ -29,19 +37,33 @@ data class MediaQueryConfig(
      * 仅在FileMedia中生效
      */
     var includeFolder : Boolean = false,
+
+
     /**
-     * 支持查询的文件格式
-     * 根据文件名称后缀 进行判断 例如:png jpg
-     * 都使用小写
+     * 是否递归出该目录下所有子文件夹数据
+     * 仅FileStore有效
+     * MediaStore会返回文件夹下所有文件,如果需要控制筛选,查询后自行筛选
      */
-    var supportMimeTypes: List<String> = mutableListOf(),
+    var recursively : Boolean = false,
+
+
     /**
-     * 是否递归出该目录下所有子文件夹数据
+     * 分页查询配置
+     * 仅MediaStore查询有效
      */
-    var recursively : Boolean = false
+    var page : Page = Page()
+
+)
 
-) {
-}
+/**
+ * MediaStore分页查询类
+ * @param pageNum 页码
+ * @param pageSize 每页查询的数量,如果为 Int.MAX_VALUE 则不分页,直接返回所有数据
+ */
+data class Page(
+    var pageNum : Int = 1,
+    var pageSize : Int = Int.MAX_VALUE
+)
 
 
 

+ 2 - 2
lib_common/src/main/java/com/convenient/android/common/media/config/SortUtils.kt

@@ -17,13 +17,13 @@ object SortUtils {
      * @param data 要排序的集合
      * @param selector 选择器,传入要排序的字段
      */
-    fun <T, R : Comparable<R>> sort(order: MediaSortOrder, data: List<T>, selector: (T) -> R?): List<T> {
+    fun <T, R : Comparable<R>> sort(order: MediaSortOrder, data: List<T>, selector: (T) -> R?): Sequence<T> {
         return when (order) {
             MediaSortOrder.ASC -> {
                 data.asSequence().sortedBy(selector)
             }
             else -> data.asSequence().sortedByDescending(selector)
-        }.toList()
+        }
     }
 
 

+ 22 - 19
lib_common/src/main/java/com/convenient/android/common/media/scan/FileStore.kt

@@ -5,10 +5,12 @@ import com.convenient.android.common.extension.listFilesInDirWithFilter
 import com.convenient.android.common.extension.toFile
 import com.convenient.android.common.media.MediaBean
 import com.convenient.android.common.media.basic.Query
-import com.convenient.android.common.media.config.SortUtils
 import com.convenient.android.common.media.config.MediaQueryConfig
 import com.convenient.android.common.media.config.MediaSortOrder
 import com.convenient.android.common.media.config.MediaSortType
+import com.convenient.android.common.media.config.SortUtils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
 import java.io.File
 
 /**
@@ -20,7 +22,7 @@ import java.io.File
 object FileStore : Query {
 
 
-    override fun query(dir: String, config: MediaQueryConfig.() -> Unit): List<MediaBean> {
+    override suspend fun query(dir: String, config: MediaQueryConfig.() -> Unit): List<MediaBean> {
         return query(dir, config = MediaQueryConfig().also(config))
     }
 
@@ -30,7 +32,7 @@ object FileStore : Query {
      * @param config 查询时的配置,指定排序等
      *
      */
-    override fun query(dir: String, config: MediaQueryConfig): List<MediaBean> {
+    override suspend fun query(dir: String, config: MediaQueryConfig): List<MediaBean> = withContext(Dispatchers.IO){
         val dirFile = if (dir.isEmpty()) Environment.getExternalStorageDirectory() else "${dir}/".toFile()
 
 
@@ -47,9 +49,9 @@ object FileStore : Query {
             }
 
             //要忽略的子文件或文件夹与当前相同,返回false
-            if (config.ignoreChildFiles.contains(it) || config.ignoreChildFiles.any { child->
-                it.startsWith(child)
-                }){
+            if (config.ignoreChildFiles.contains(it) || config.ignoreChildFiles.any { child ->
+                    it.startsWith(child)
+                }) {
                 return@listFilesInDirWithFilter false
             }
             //如果没有指定需要的文件格式,则全部返回
@@ -61,24 +63,25 @@ object FileStore : Query {
         }) ?: emptyList()
 
         //对文件进行排序
-        return sortList(
+        sortList(
             order = config.order,
             sortType = config.sortType,
             data = files,
             sortByDateSelector = { it.lastModified() },
             sortByNameSelector = { it.name },
             sortBySizeSelector = { it.length() },
-        ).map {
-            MediaBean(
-                mediaPath = it.absolutePath,
-                name = it.name,
-                extension = it.extension,
-                isFile = it.isFile,
-                lastModified = it.lastModified(),
-                length = it.length(),
-                parentPath = it.parent ?: ""
-            )
-        }.toList()
+        ).sortedBy { it.isFile }
+            .map {
+                MediaBean(
+                    mediaPath = it.absolutePath,
+                    name = it.name,
+                    extension = it.extension,
+                    isFile = it.isFile,
+                    lastModified = if (it.isFile) it.lastModified() else 0L,
+                    length = it.length(),
+                    parentPath = it.parent ?: ""
+                )
+            }.toList()
 
     }
 
@@ -104,7 +107,7 @@ object FileStore : Query {
         sortByNameSelector: (T) -> S?,
         sortByDateSelector: (T) -> R?,
         sortBySizeSelector: (T) -> R?,
-    ): List<T> {
+    ): Sequence<T> {
         return when (sortType) {
             MediaSortType.DATE -> SortUtils.sort(order, data, sortByDateSelector)
             MediaSortType.NAME -> SortUtils.sort(order, data, sortByNameSelector)

+ 121 - 66
lib_common/src/main/java/com/convenient/android/common/media/scan/MediaStore.kt

@@ -1,16 +1,22 @@
 package com.convenient.android.common.media.scan
 
+import android.content.ContentResolver
 import android.content.ContentUris
-import android.content.Context
-import android.net.Uri
+import android.database.Cursor
+import android.os.Build
+import android.os.Bundle
 import android.provider.MediaStore
 import android.util.Log
+import androidx.annotation.RequiresApi
 import com.convenient.android.common.config.MyPdfBaseModule
 import com.convenient.android.common.extension.toFile
 import com.convenient.android.common.media.MediaBean
 import com.convenient.android.common.media.basic.Query
 import com.convenient.android.common.media.config.MediaQueryConfig
+import com.convenient.android.common.media.config.MediaSortOrder
 import com.convenient.android.common.media.config.MediaSortType
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
 
 /**
  * @classname:
@@ -33,14 +39,58 @@ object MediaStore : Query {
         MediaStore.Files.FileColumns.DISPLAY_NAME
     )
 
-
-    override fun query(dir: String, config: MediaQueryConfig.() -> Unit): List<MediaBean> {
+    override suspend fun query(dir: String, config: MediaQueryConfig.() -> Unit): List<MediaBean> {
         return query(dir, MediaQueryConfig().also(config))
     }
 
 
-    override fun query(dir: String, config: MediaQueryConfig): List<MediaBean> {
+    /**
+     * 查询媒体文件
+     */
+    override suspend fun query(dir: String, config: MediaQueryConfig): List<MediaBean> = withContext(Dispatchers.IO) {
+        try {
+            val cursor: Cursor? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                getQueryCursorWithMoreThanTheApi26(dir, config)
+            } else {
+                getQueryCursorWithLessThanApi26(dir, config)
+            }
+            val list = mutableListOf<MediaBean>()
+            cursor?.apply {
+                while (moveToNext()) {
+                    val path = getString(getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA))
+                    val mediaFile = path.toFile()
+                    if (mediaFile?.isFile?.not() == true) {
+                        continue
+                    }
 
+                    val id = getLong(getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
+                    val name = getString(getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME))
+                    val contentUri = ContentUris.withAppendedId(MediaStore.Files.getContentUri("external"), id)
+
+                    list.add(
+                        MediaBean(
+                            mediaPath = path,
+                            name = if (name.isNullOrEmpty()) mediaFile?.name ?: "" else name,
+                            extension = mediaFile?.extension ?: "",
+                            isFile = true,
+                            lastModified = mediaFile?.lastModified() ?: 0,
+                            parentPath = mediaFile?.parent ?: "",
+                            length = mediaFile?.length(),
+                            uri = contentUri
+                        )
+                    )
+                }
+            }
+
+            cursor?.close()
+            list
+        } catch (e: Exception) {
+            e.printStackTrace()
+            emptyList<MediaBean>()
+        }
+    }
+
+    private fun getQueryWhere(dir: String, config: MediaQueryConfig): String {
         val selectBuilder = StringBuilder()
         val selectionList = mutableListOf<String>()
         //文件夹查询限制
@@ -48,6 +98,24 @@ object MediaStore : Query {
         if (dirSelection.isNotEmpty()) {
             selectionList.add(dirSelection)
         }
+
+        if (config.ignoreChildFiles.isNotEmpty()) {
+            //要忽略的文件
+            val files = config.ignoreChildFiles
+            val ignoreSelect = StringBuilder()
+            files.forEachIndexed { index, file ->
+                when {
+                    index == 0 && files.size != 1 -> ignoreSelect.append("(${MediaStore.Files.FileColumns.DATA} NOT LIKE '%${file.absolutePath}%'")
+                    index == files.lastIndex && files.size != 1 -> ignoreSelect.append(" and ${MediaStore.Files.FileColumns.DATA} not LIKE '%${file.absolutePath}%' )")
+                    files.size == 1 -> ignoreSelect.append("(${MediaStore.Files.FileColumns.DATA} not LIKE '%${file.absolutePath}%')")
+                    else -> ignoreSelect.append(" and ${MediaStore.Files.FileColumns.DATA} not LIKE '%${file.absolutePath}%'")
+                }
+            }
+            if (ignoreSelect.isNotEmpty()) {
+                selectionList.add(ignoreSelect.toString())
+            }
+        }
+        selectionList.add(" (${MediaStore.Files.FileColumns.MIME_TYPE} not like 'null' ) ")
         //添加查询的文件格式限制
         val mimeTypesSelection = getMimeTypesSelection(config.supportMimeTypes)
         if (mimeTypesSelection.isNotEmpty()) {
@@ -61,67 +129,67 @@ object MediaStore : Query {
                 selectBuilder.append(s)
             }
         }
-        val sort = getSortSubStatement(config)
+        return selectBuilder.toString()
+    }
 
-        Log.e("MediaStore", "查询语句:$selectBuilder")
-        Log.e("MediaStore", "排序语句:${sort}")
+    /**
+     * 获取小于API26的查询游标
+     */
+    private fun getQueryCursorWithLessThanApi26(dir: String, config: MediaQueryConfig): Cursor? {
+
+        //排序子语句
+        var sort = getSortSubStatement(config)
+        if (config.page.pageSize != Int.MAX_VALUE) {
+            //如果设置的不是最大数量,则需要分页查询
+            sort += " LIMIT ${config.page.pageSize} OFFSET ${(config.page.pageNum - 1) * config.page.pageSize}"
+        }
 
-        val cursor = MyPdfBaseModule.getAppContext()?.contentResolver?.query(
+        val selectBuilder = getQueryWhere(dir, config)
+        return MyPdfBaseModule.getAppContext()?.contentResolver?.query(
             MediaStore.Files.getContentUri("external"),
             projection,
-            selectBuilder.toString(),
+            selectBuilder,
             null,
             sort
         )
+    }
 
-        val list = mutableListOf<MediaBean>()
-        cursor?.apply {
-            try {
-                while (moveToNext()) {
-                    val path = getString(getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA))
-                    val mediaFile = path.toFile()
-                    if (mediaFile?.isFile?.not() == true) {
-                        continue
-                    }
-                    if (config.ignoreChildFiles.any {
-                            path.startsWith(it.absolutePath)
-                        }) {
-                        //如果是忽略的文件或文件夹,直接跳过
-                        continue
-                    }
-
-                    if (config.recursively.not() && mediaFile?.parent?.equals(dir)?.not() == true) {
-                        //没有开启递归子目录,并且文件所在的文件夹与扫描的目录不一致,则跳过
-                        continue
+    /**
+     * 获取高于(包含)Api26的查询游标
+     */
+    @RequiresApi(Build.VERSION_CODES.O)
+    private fun getQueryCursorWithMoreThanTheApi26(dir: String, config: MediaQueryConfig): Cursor? {
+        val queryArgs = Bundle().apply {
+            if (config.page.pageSize != Int.MAX_VALUE) {
+                putInt(ContentResolver.QUERY_ARG_LIMIT, config.page.pageSize)
+                putInt(ContentResolver.QUERY_ARG_OFFSET, (config.page.pageNum - 1) * config.page.pageSize)
+            }
+            putInt(
+                ContentResolver.QUERY_ARG_SORT_DIRECTION,
+                if (config.order == MediaSortOrder.DESC) ContentResolver.QUERY_SORT_DIRECTION_DESCENDING else ContentResolver.QUERY_SORT_DIRECTION_ASCENDING
+            )
+            putStringArray(
+                ContentResolver.QUERY_ARG_SORT_COLUMNS, arrayOf(
+                    when (config.sortType) {
+                        MediaSortType.NAME -> MediaStore.Files.FileColumns.DISPLAY_NAME
+                        MediaSortType.DATE -> MediaStore.Files.FileColumns.DATE_MODIFIED
+                        MediaSortType.SIZE -> MediaStore.Files.FileColumns.SIZE
                     }
-
-                    val id = getLong(getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
-                    val name = getString(getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME))
-                    val contentUri = ContentUris.withAppendedId(MediaStore.Files.getContentUri("external"), id)
-
-                    list.add(
-                        MediaBean(
-                            mediaPath = path,
-                            name = name,
-                            extension = mediaFile?.extension ?: "",
-                            isFile = true,
-                            lastModified = mediaFile?.lastModified()?:0,
-                            parentPath = mediaFile?.parent ?: "",
-                            length = mediaFile?.length(),
-                            uri = contentUri
-                        )
-                    )
-
-                }
-            } catch (e: Exception) {
-                e.printStackTrace()
+                )
+            )
+            val where = getQueryWhere(dir, config)
+            if (where.isNotEmpty()) {
+                Log.e("MediaStore", "查询语句:$where")
+                putString(ContentResolver.QUERY_ARG_SQL_SELECTION, where)
             }
         }
-        cursor?.close()
-
-        return list
+        return MyPdfBaseModule.getAppContext()?.contentResolver?.query(
+            MediaStore.Files.getContentUri("external"),
+            projection, queryArgs, null
+        )
     }
 
+
     /**
      * 查询指定目录的子语句
      * ( _data LIKE '/storage/emulated/0/DCIM/%' )
@@ -157,7 +225,6 @@ object MediaStore : Query {
      * _size desc or asc
      */
     private fun getSortSubStatement(config: MediaQueryConfig): String {
-
         return when (config.sortType) {
             MediaSortType.NAME -> "${MediaStore.Files.FileColumns.DISPLAY_NAME} ${config.order.name}"
             MediaSortType.DATE -> "${MediaStore.Files.FileColumns.DATE_MODIFIED} ${config.order.name}"
@@ -166,16 +233,4 @@ object MediaStore : Query {
     }
 
 
-    fun queryContentUriFilePath(context: Context, uri: Uri): String {
-        var cursor = context.contentResolver.query(uri, projection, null, null, null)
-
-        if (cursor?.moveToFirst() == true) {
-            val path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA))
-            return path
-        } else {
-            return ""
-        }
-
-    }
-
 }

+ 72 - 0
lib_common/媒体文件扫描文档.md

@@ -0,0 +1,72 @@
+# 媒体文件扫描
+
+媒体文件扫描通过两种方式进行扫描 **File**、**MediaStore**, 可通过**MediaQueryConfig.kt**指定扫描的配置选项
+
+## - FileStore
+通过File listFiles进行扫描文件
+```
+    FileStore.query(dir : String, config : MediaQueryConfig) : List<MediaBean>
+```
+
+## - MediaStore
+通过MediaStore Api进行文件扫描
+```
+    MediaStore.query(dir : String, config : MediaQueryConfig) : List<MediaBean>
+```
+
+## - MediaQueryConfig
+支持的配置选项如下:
+- 排序方式
+- 排序类型
+- 忽略要查询路径下的文件或文件夹
+- 指定查询的文件格式
+- 结果中是否包含文件夹(仅FileStore有效)
+- 递归所有文件夹(仅FileStore可配置false, MediaStore递归所有)
+- 分页配置(仅MediaStore有效)
+```
+data class MediaQueryConfig{
+    //升序、降序
+    var order : MediaSortOrder = MediaSortOrder.DESC
+
+    //排序方式
+    var sortType : MediaSortType = MediaSortOrder.DATE
+
+    //忽略的子文件夹、文件
+    ignoreChildFiles : List<File> = mutableListOf()
+
+    //支持的文件格式,默认空为不限制,查询所有格式 png,jpg,pdf...
+    var supportMimeTypes : List<String> = mutableListOf()
+
+    //结果是否包含文件夹,仅FileStore生效
+    includeFolder : Boolean = false
+
+    //遍历子文件夹内容,仅FileStore生效
+    //MediaStore默认遍历子文件夹所有文件,配置不生效
+    var recursively : Boolean = false
+
+    //分页配置,仅MediaStore查询生效
+    var page : Page = Page()
+}
+
+//pageSize 如果为Int.MAX_VALUE 不进行分页,直接查询所有
+data class Page(
+    var pageNum : Int = 1,
+    var pageSize : Int = Int.MAX_VALUE
+)
+```
+
+## - MediaBean
+```
+data class MediaBean(
+    var mediaPath : String,//媒体文件路径
+    var name : String, //文件名称
+    var extension : String, //文件格式
+    var isFile : Boolean, //是否是文件
+    var lastModified : Long, //最后修改时间
+    var parentPath : String, //父文件夹路径
+    var length : Long, //文件大小
+    var uri : Uri? = null, //媒体文件Uri
+)
+
+```
+