前情回顾
之前出过一篇扫描文件的文章,文章--->juejin.cn/post/738722... 。
该文章中有详细描述和代码可以直接使用,不过文章中扫描比较费时,因为是单线处理,手机中全部文件都要挨个过一遍,对于外部文件夹的扫描总体来说还是比较快的。
在我用了四年的手机上对外部文件夹扫描图片能找到三千多张图片用时约半分钟,但对于data内部文件夹扫描就非常的耗时,因为data文件夹的访问方式不同,需要利用uri和DocumentFile来进行文件的访问,虽然在我的手机中总共能扫描到一万四千多张图片,但需要耗时20多分钟半个小时甚至更久,之前因为小体量工具APP开发成本的问题就没太深究逻辑,但现在有充足的时间所以对代码逻辑进行了一系列优化,其中外部文件夹扫描时长可以优化到几秒内,而内部文件夹扫描则可以优化到以前的1/4或1/5甚至更少,在我的手机中已压缩到4分多钟5分钟即可全部扫描完成。
新的扫描逻辑把根目录下的所有文件夹扫描分成各个协程同步并行扫描,扫描结果通过Flow,LiveData和ViewModel存储并更新UI。本文中未主动提及的工具类均使用AndroidUtilCode实现,LiveData/Flow+ViewModel是LifeCycle包,具体引入方式可自行查找接入,或者也可继续使用旧文章中的Handler实现数据保存和UI更新。
需求描述
通过遍历手机内部全部文件夹来获取你想要的文件,比如图片,这个需求应用场景可以作为文件管理或其他文件处理app比如文件恢复功能。
领导要实现的需求是查找被删除的图片,这个我们确实办不到,手机中相册删除后会到回收站,这个时候还不叫做真正的删除,真正的删除操作不是简单的两行代码就能给找回的,至少我是做不到。
那我可不可以通过扫描手机里的所有图片文件展示给用户让用户自行去寻找,通过代码遍历到的图片应该有很多是用户平时非正常途径看不到的,也可能有一些用户已删除的图片被某些app备份到其他目录被找出来,所以应该也算完成了需求,当然只要你不保证给用户一定能找到他丢失的文件严格意义上来说这就不是假功能。
流程上面虽然和上面贴的文章一样,但是为了方便使用这里还是继续按步骤走一遍
一、权限申请逻辑
由于Android对于权限把控的严格控制,在Android不同版本下需要对扫描逻辑以及权限做不同的处理。 (扫描完成你可能会在你的手机里发现很多有趣的图片或者其他东西~~~尤其是data文件夹扫描,一些软件的缓存图片可能你都不知道)
1、Android11以下设备
如果当前设备在Android11以下则仅需要申请WRITE_EXTERNAL_STORAGE,READ_EXTERNAL_STORAGE读写权限即可遍历访问全部文件夹。
2、Android11以上设备
Android11以上设备如果想访问手机内的全部文件夹则需要获取《全部文件访问权限》,这个权限无法通过弹窗的方式点击允许,需要跳转系统设置页面手动点开开关。
而在Android11及以上设备中,获取到全部文件访问权限并不代表你可以访问到全部文件,其中重要的包含android/data文件夹是无法正常访问的,这个时候我们还需要去申请data文件夹的访问权限如下图,包括这个文件夹下的文件遍历也与其他文件夹不同,下面可自行看代码。
二、扫描流程及代码
1、权限申请
Manifest.xml静态申请
kotlin
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
Android11以下申请普通读写权限
kotlin
/**
* 申请普通读写权限
*/
private fun permissionReadWrite() {
//请求读写权限
PermissionUtils.permission(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
.callback(object : PermissionUtils.SimpleCallback {
override fun onGranted() {
//已经获得权限则直接进入扫描
startScan()
}
override fun onDenied() {
showError("Permission denied. Unable to use the feature.")
}
}).request()
}
申请全部文件访问权限
kotlin
//检查是否拥有全部文件访问权限
if (!Environment.isExternalStorageManager()) {
var intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.data = Uri.parse("package:$packageName")
startActivityForResult(intent, 9970)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
//获取全部文件权限返回
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
//权限获取成功
} else {
showError("Permission denied. Unable to use the feature.")
}
}
申请Data文件夹访问权限
kotlin
/**
* 判断是否有android/data文件夹权限
*/
fun isGrantAndroidDataDirPermission(): Boolean {
//如果当前小于android11就默认获取成功吧,因为android11以下不需要获取data文件夹权限就可以直接访问
if ( Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
return true
}
val uri =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU){//Android13及以上的data文件夹uri有所变化,所以需要分开处理
"content://com.android.externalstorage.documents/tree/primary%3AA%E2%80%8Bndroid%2Fdata"
// "content://com.android.externalstorage.documents/tree/primary%3AA%E2%80%8Bndroid%2Fdata/document/primary%3AA%E2%80%8Bndroid%2Fdata"
}else{
"content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata"
// "content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3AAndroid%2Fdata"
}
val uriPermissionList = Utils.getApp().contentResolver.persistedUriPermissions
for (persistedUriPermission in uriPermissionList) {
if (persistedUriPermission.isReadPermission && persistedUriPermission.uri.toString() == uri) {
return true
}
}
return false
}
if (!isGrantAndroidDataDirPermission()) {
//获取data目录访问权限
val uri =
Uri.parse(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
"content://com.android.externalstorage.documents/tree/primary%3AA%E2%80%8Bndroid%2Fdata"
} else{
"content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata"
}
)
val documentFile = DocumentFile.fromTreeUri(this@ScanModeChooseActivity, uri)
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.flags = (Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
documentFile?.let {
intent.putExtra(
DocumentsContract.EXTRA_INITIAL_URI,
documentFile.uri
)
startActivityForResult(intent, 9969)
} ?: let {
showError("Permission denied. Unable to use the feature.")
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 9969) {//data文件夹权限获取结果返回
var uri: Uri? = data?.data
uri?.let {
if (data?.flags != null) {//获取成功
//保存data权限的获取状态
contentResolver.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
//跳转扫描
} else {//获取失败
showError("Permission denied. Unable to use the feature.")
}
} ?: let {//获取失败
showError("Permission denied. Unable to use the feature.")
}
}
}
2、扫描过程
这里新建了个工具类ScanFileNewUtil,是在旧文章工具类中做的修改,内部同样分为普通扫描和深度扫描,普通扫描中直接拿取android外部存储文件根目录,把根目录下文件夹拆分并创建每一个协程,并行执行所有的协程并等待全部协程执行完毕就代表扫描完毕,然后可进入下一流程。深度扫描时则仅对data文件夹下的各个文件夹做拆分创建协程,而外部文件夹仅使用一个协程扫描,因为data内部文件夹一般情况都会比扫描整个外部文件夹要慢,所以就没必要再分配资源去快速处理外部文件夹。下面放完整代码
ScanFileNewUtil工具类------扫描结果返回的FileEntity对象中如果是外部文件夹扫描到的参数isDocumentFile就为false,如果是data文件夹内部通过DocumentFile扫描到的参数isDocumentFile就为true,两种数据不一样,需要区分后在对获取到的文件处理,比如预览,通过外部文件夹扫描获取到的文件路径filePath可以直接使用,而DocumentFile则只能使用URI,文件的URI参数已保存到对象的uriStr中自行使用。
kotlin
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Environment
import androidx.documentfile.provider.DocumentFile
import com.blankj.utilcode.util.ConvertUtils
import com.blankj.utilcode.util.FileUtils
import com.blankj.utilcode.util.TimeUtils
import com.sugoilab.restore.bean.FileEntity
import com.sugoilab.restore.viewmodel.FileScanViewModel
import com.sugoilab.restore.viewmodel.PathTextViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
/**
* 使用协程来扫描手机内部所有文件,多协程并发处理
*/
class ScanFileNewUtil(
val context: Context,
private var fileScanViewModel: FileScanViewModel?,
private var pathTextViewModel: PathTextViewModel?
) {
//扫描文件任务量大,属于CPU密集型操作需使用Dispatchers.Default
private val scope = CoroutineScope(Dispatchers.Default)
private var mainJob: Job? = null
/**
* 从全部的手机文件进行搜索------普通扫描,不扫描data文件夹
*/
suspend fun getAllFilesNormal(type: String) {
mainJob = scope.launch {
try {
//扫描外部文件信息
//只对外部文件夹扫描,把根目录各文件夹分协程并行扫描节省数倍时间
val paramFile = Environment.getExternalStorageDirectory()
if (paramFile == null || !FileUtils.isDir(paramFile)) return@launch
val arrayOfFile = paramFile.listFiles()
if (arrayOfFile != null && arrayOfFile.isNotEmpty()) {
val j = arrayOfFile.size
for (i in 0 until j) {
val file = arrayOfFile[i]
if (file.isDirectory) {
//创建多少个子协程全看有多少主目录下有多少个文件夹,这些文件夹并行扫描
launch {
listFilesInDirWithFilterNew(file, type)
}
} else {
if (file.absolutePath.contains(context.packageName) || file.absolutePath.contains(
"SugoilabFileRecovery"
)
) {
continue
}
//判断并获取当前扫描的文件是否是对应类型文件
val fileEntity: FileEntity =
findSingleFileUpdateUi(file, type) ?: continue
//文件获取成功提醒线程更新
fileScanViewModel?.setData(fileEntity)
}
}
}
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}
mainJob!!.join()
}
/**
* 从全部的手机文件进行搜索------深度扫描
*/
suspend fun getAllFilesDeep(type: String) {
mainJob = scope.launch {
try {
//先扫描外部文件信息
launch {
//深度扫描没必要把外部文件按文件夹分不同协程并行,因为内部Data文件夹扫描方式相比外部文件夹来说很慢,没必要额外占用多个协程处理
listFilesInDirWithFilterNew(Environment.getExternalStorageDirectory(), type)
}
//再扫描Android data文件夹信息,扫描方式不同
val dirUri =
Uri.parse(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
"content://com.android.externalstorage.documents/tree/primary%3AA%E2%80%8Bndroid%2Fdata"
} else {
"content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata"
}
)
val documentFile = DocumentFile.fromTreeUri(context, dirUri)
//遍历DocumentFile,但是要避开本app的缓存目录
val files = documentFile?.listFiles()
files?.let {
for (file in files) {
if (file.uri.path?.contains(context.packageName) == true) {
continue
}
if (file.isDirectory)//如果是目录则重复本方法往目录下执行。
//创建多少个子协程全看有多少主目录下有多少个文件夹,这些文件夹并行扫描
launch {
getFileFromDirByDocumentFile(file, type)
}
else {//如果是文件则判断文件类型
try {
val fileEntity =
getSingleFileByDocumentFile(file, type) ?: continue
//文件获取成功提醒线程更新
fileScanViewModel?.setData(fileEntity)
} catch (e: Exception) {
e.printStackTrace();
}
}
}
}
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}
mainJob!!.join()
}
private fun listFilesInDirWithFilterNew(
paramFile: File?,
type: String
) {
if (paramFile == null || !FileUtils.isDir(paramFile)) return
val arrayOfFile = paramFile.listFiles()
if (arrayOfFile != null && arrayOfFile.isNotEmpty()) {
val j = arrayOfFile.size
for (i in 0 until j) {
val file = arrayOfFile[i]
//返回当前正在扫描的文件路径
pathTextViewModel?.setData(file.absolutePath)
if (file.isDirectory) {
listFilesInDirWithFilterNew(file, type)
} else {
if (file.absolutePath.contains(context.packageName) || file.absolutePath.contains(
"SugoilabFileRecovery"
)
) {
continue
}
//判断并获取当前扫描的文件是否是对应类型文件
val fileEntity: FileEntity = findSingleFileUpdateUi(file, type) ?: continue
//文件获取成功提醒数据更新
fileScanViewModel?.setData(fileEntity)
}
}
}
}
private fun findSingleFileUpdateUi(paramFile: File, type: String): FileEntity? {
try {
val fileEntity = FileEntity(UI_TYPE_MAIN)
val filePathLowCase = paramFile.absolutePath.lowercase(Locale.getDefault())
val fileType: String = CheckFileTypeUtil.getFileType(paramFile.absolutePath)
when (type) {
TYPE_IMAGE_TEXT -> {
if (filePathLowCase.endsWith(".bmp") || filePathLowCase.endsWith(".jpg") || filePathLowCase.endsWith(
".png"
) || filePathLowCase.endsWith(
".tif"
) || filePathLowCase.endsWith(".gif") || filePathLowCase.endsWith(".webp") || fileType.equals(
"bmp",
ignoreCase = true
) || fileType.equals("jpg", ignoreCase = true) || fileType.equals(
"png",
ignoreCase = true
) || fileType.equals("tif", ignoreCase = true) || fileType.equals(
"gif",
ignoreCase = true
) || fileType.equals("webp", ignoreCase = true)
) {
} else {
return null
}
}
TYPE_VIDEO_TEXT -> {
if (filePathLowCase.endsWith(".mp4") || filePathLowCase.endsWith(".avi") || filePathLowCase.endsWith(
".3gp"
) || filePathLowCase.endsWith(
".wmv"
) || filePathLowCase.endsWith(".flv") || filePathLowCase.endsWith(".rmvb") || filePathLowCase.endsWith(
".mov"
)
|| fileType.equals("flv", ignoreCase = true) || fileType.equals(
"wmv",
ignoreCase = true
) || fileType.equals("3gp", ignoreCase = true) || fileType.equals(
"avi",
ignoreCase = true
) || fileType.equals("mp4", ignoreCase = true) || fileType.equals(
"rmvb",
ignoreCase = true
) || fileType.equals(
"mov",
ignoreCase = true
)
) {
} else {
return null
}
}
TYPE_AUDIO_TEXT -> {
if (filePathLowCase.endsWith(".mp3") || filePathLowCase.endsWith(".amr") || filePathLowCase.endsWith(
".m4a"
) || filePathLowCase.endsWith(
".wav"
) || filePathLowCase.endsWith(".aac") || filePathLowCase.endsWith(".ogg") || fileType.equals(
"mp3",
ignoreCase = true
) || fileType.equals("amr", ignoreCase = true) || fileType.equals(
"m4a",
ignoreCase = true
) || fileType.equals("wav", ignoreCase = true) || fileType.equals(
"aac",
ignoreCase = true
) || fileType.equals("ogg", ignoreCase = true)
) {
} else {
return null
}
}
TYPE_DOC_TEXT -> {
if (filePathLowCase.endsWith(".doc") || filePathLowCase.endsWith(".docx")) {
} else {
return null
}
}
TYPE_PDF_TEXT -> {
if (filePathLowCase.endsWith(".pdf")) {
} else {
return null
}
}
TYPE_XLS_TEXT -> {
if (filePathLowCase.endsWith(".xls") || filePathLowCase.endsWith(".xlsx")) {
} else {
return null
}
}
TYPE_PPT_TEXT -> {
if (filePathLowCase.endsWith(".ppt") || filePathLowCase.endsWith(".pptx")) {
} else {
return null
}
}
TYPE_TXT_TEXT -> {
if (filePathLowCase.endsWith(".txt")) {
} else {
return null
}
}
TYPE_ALL_WENBEN_TEXT -> {
if (filePathLowCase.endsWith(".doc") || filePathLowCase.endsWith(".docx") || filePathLowCase.endsWith(
".pdf"
) || filePathLowCase.endsWith(
".xls"
) || filePathLowCase.endsWith(".xlsx") || filePathLowCase.endsWith(".ppt") || filePathLowCase.endsWith(
".pptx"
) || filePathLowCase.endsWith(".txt")
) {
} else {
return null
}
}
TYPE_COMPRESS_TEXT -> {
if (filePathLowCase.endsWith(".zip") && filePathLowCase.endsWith(".rar") || filePathLowCase.endsWith(
".7z"
)
) {
} else {
return null
}
}
else -> {
return null
}
}
fileEntity.dirTypeStr = getDirTypeStrByPath(paramFile.absolutePath)
fileEntity.filePath = paramFile.absolutePath
fileEntity.fileName = paramFile.name
fileEntity.fileType = type
fileEntity.fileRealType = fileType
fileEntity.fileLastModifiedTime = getFileLastModifiedTime(paramFile)
fileEntity.fileLastModifiedTimeMils = paramFile.lastModified()
fileEntity.fileSizeStr = FileUtils.getSize(paramFile)
fileEntity.fileSizeLong = FileUtils.getLength(paramFile)
return fileEntity
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
private fun getFileFromDirByDocumentFile(
documentFile: DocumentFile?,
type: String
) {
//遍历DocumentFile,但是要避开本app的缓存目录,不然会出现很多重复图片
val files = documentFile?.listFiles()
files?.let {
for (file in files) {
if (file.uri.path?.contains(context.packageName) == true) {
continue
}
//返回当前正在扫描的文件路径
pathTextViewModel?.setData(file.uri.path ?: "")
if (file.isDirectory)//如果是目录则重复本方法往目录下执行。
getFileFromDirByDocumentFile(file, type)
else {//如果是文件则判断文件类型
try {
val fileEntity = getSingleFileByDocumentFile(file, type) ?: continue
//文件获取成功提醒线程更新
fileScanViewModel?.setData(fileEntity)
} catch (e: Exception) {
e.printStackTrace();
}
}
}
}
}
private fun getSingleFileByDocumentFile(
file: DocumentFile,
type: String
): FileEntity? {
file.type?.let { fileType ->
when (type) {
TYPE_IMAGE_TEXT -> {
if (fileType.contains("image")) {
val fileEntity = FileEntity(UI_TYPE_MAIN)
//表示是图片文件,则做保存
fileEntity.fileName = file.name
fileEntity.fileType = type
fileEntity.fileLastModifiedTime = TimeUtils.millis2String(
file.lastModified(),
SimpleDateFormat("dd/MM/yyyy")
)
fileEntity.fileLastModifiedTimeMils = file.lastModified()
fileEntity.fileSizeStr = ConvertUtils.byte2FitMemorySize(file.length())
fileEntity.fileSizeLong = file.length()
fileEntity.fileRealType = "jpg"
fileEntity.isDataFile = true
fileEntity.dirTypeStr = "data"
fileEntity.isDocumentFile = true
fileEntity.uriStr = file.uri.toString()
return fileEntity
}
}
TYPE_VIDEO_TEXT -> {
if (fileType.contains("video")) {
val fileEntity = FileEntity(UI_TYPE_MAIN)
fileEntity.fileName = file.name
fileEntity.fileType = type
fileEntity.fileLastModifiedTime = TimeUtils.millis2String(
file.lastModified(),
SimpleDateFormat("dd/MM/yyyy")
)
fileEntity.fileLastModifiedTimeMils = file.lastModified()
fileEntity.fileSizeStr = ConvertUtils.byte2FitMemorySize(file.length())
fileEntity.fileSizeLong = file.length()
fileEntity.fileRealType = "mp4"
fileEntity.isDataFile = true
fileEntity.dirTypeStr = "data"
fileEntity.isDocumentFile = true
fileEntity.uriStr = file.uri.toString()
return fileEntity
}
}
TYPE_AUDIO_TEXT -> {
if (fileType.contains("audio")) {
val fileEntity = FileEntity(UI_TYPE_MAIN)
fileEntity.fileName = file.name
fileEntity.fileType = type
fileEntity.fileLastModifiedTime = TimeUtils.millis2String(
file.lastModified(),
SimpleDateFormat("dd/MM/yyyy")
)
fileEntity.fileLastModifiedTimeMils = file.lastModified()
fileEntity.fileSizeStr = ConvertUtils.byte2FitMemorySize(file.length())
fileEntity.fileSizeLong = file.length()
fileEntity.fileRealType = "mp3"
fileEntity.isDataFile = true
fileEntity.dirTypeStr = "data"
fileEntity.isDocumentFile = true
fileEntity.uriStr = file.uri.toString()
return fileEntity
}
}
TYPE_DOC_TEXT -> {
if (fileType.equals("application/msword") || fileType.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document")) {
val fileEntity = FileEntity(UI_TYPE_MAIN)
fileEntity.fileName = file.name
fileEntity.fileType = type
fileEntity.fileLastModifiedTime = TimeUtils.millis2String(
file.lastModified(),
SimpleDateFormat("dd/MM/yyyy")
)
fileEntity.fileLastModifiedTimeMils = file.lastModified()
fileEntity.fileSizeStr = ConvertUtils.byte2FitMemorySize(file.length())
fileEntity.fileSizeLong = file.length()
if (fileType.equals("application/msword"))
fileEntity.fileRealType = "doc"
else
fileEntity.fileRealType = "docx"
fileEntity.isDataFile = true
fileEntity.dirTypeStr = "data"
fileEntity.isDocumentFile = true
fileEntity.uriStr = file.uri.toString()
return fileEntity
}
}
TYPE_PDF_TEXT -> {
if (fileType.equals("application/pdf")) {
val fileEntity = FileEntity(UI_TYPE_MAIN)
fileEntity.fileName = file.name
fileEntity.fileType = type
fileEntity.fileLastModifiedTime = TimeUtils.millis2String(
file.lastModified(),
SimpleDateFormat("dd/MM/yyyy")
)
fileEntity.fileLastModifiedTimeMils = file.lastModified()
fileEntity.fileSizeStr = ConvertUtils.byte2FitMemorySize(file.length())
fileEntity.fileSizeLong = file.length()
if (fileType.equals("application/pdf"))
fileEntity.fileRealType = "pdf"
fileEntity.isDataFile = true
fileEntity.dirTypeStr = "data"
fileEntity.isDocumentFile = true
fileEntity.uriStr = file.uri.toString()
return fileEntity
}
}
TYPE_XLS_TEXT -> {
if (fileType.equals("application/vnd.ms-excel") || fileType.equals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) {
val fileEntity = FileEntity(UI_TYPE_MAIN)
fileEntity.fileName = file.name
fileEntity.fileType = type
fileEntity.fileLastModifiedTime = TimeUtils.millis2String(
file.lastModified(),
SimpleDateFormat("dd/MM/yyyy")
)
fileEntity.fileLastModifiedTimeMils = file.lastModified()
fileEntity.fileSizeStr = ConvertUtils.byte2FitMemorySize(file.length())
fileEntity.fileSizeLong = file.length()
if (fileType.equals("application/vnd.ms-excel"))
fileEntity.fileRealType = "xls"
else
fileEntity.fileRealType = "xlsx"
fileEntity.isDataFile = true
fileEntity.dirTypeStr = "data"
fileEntity.isDocumentFile = true
fileEntity.uriStr = file.uri.toString()
return fileEntity
}
}
TYPE_PPT_TEXT -> {
if (fileType.equals("application/vnd.ms-powerpoint") || fileType.equals("application/vnd.openxmlformats-officedocument.presentationml.presentation")) {
val fileEntity = FileEntity(UI_TYPE_MAIN)
fileEntity.fileName = file.name
fileEntity.fileType = type
fileEntity.fileLastModifiedTime = TimeUtils.millis2String(
file.lastModified(),
SimpleDateFormat("dd/MM/yyyy")
)
fileEntity.fileLastModifiedTimeMils = file.lastModified()
fileEntity.fileSizeStr = ConvertUtils.byte2FitMemorySize(file.length())
fileEntity.fileSizeLong = file.length()
if (fileType.equals("application/vnd.ms-powerpoint"))
fileEntity.fileRealType = "ppt"
else
fileEntity.fileRealType = "pptx"
fileEntity.isDataFile = true
fileEntity.dirTypeStr = "data"
fileEntity.isDocumentFile = true
fileEntity.uriStr = file.uri.toString()
return fileEntity
}
}
TYPE_TXT_TEXT -> {
if (fileType.equals("text/plain")) {
val fileEntity = FileEntity(UI_TYPE_MAIN)
fileEntity.fileName = file.name
fileEntity.fileType = type
fileEntity.fileLastModifiedTime = TimeUtils.millis2String(
file.lastModified(),
SimpleDateFormat("dd/MM/yyyy")
)
fileEntity.fileLastModifiedTimeMils = file.lastModified()
fileEntity.fileSizeStr = ConvertUtils.byte2FitMemorySize(file.length())
fileEntity.fileSizeLong = file.length()
fileEntity.fileRealType = "txt"
fileEntity.isDataFile = true
fileEntity.dirTypeStr = "data"
fileEntity.isDocumentFile = true
fileEntity.uriStr = file.uri.toString()
return fileEntity
}
}
TYPE_ALL_WENBEN_TEXT -> {
if (fileType.equals("application/msword") || fileType.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document") || fileType.equals(
"application/pdf"
) || fileType.equals("application/vnd.ms-excel") || fileType.equals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") || fileType.equals(
"application/vnd.ms-powerpoint"
) || fileType.equals("application/vnd.openxmlformats-officedocument.presentationml.presentation") || fileType.equals(
"text/plain"
)
) {
val fileEntity = FileEntity(UI_TYPE_MAIN)
fileEntity.fileName = file.name
fileEntity.fileType = type
fileEntity.fileLastModifiedTime = TimeUtils.millis2String(
file.lastModified(),
SimpleDateFormat("dd/MM/yyyy")
)
fileEntity.fileLastModifiedTimeMils = file.lastModified()
fileEntity.fileSizeStr = ConvertUtils.byte2FitMemorySize(file.length())
fileEntity.fileSizeLong = file.length()
fileEntity.fileRealType =
when (fileType) {
"application/msword" -> {
"doc"
}
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" -> {
"docx"
}
"application/pdf" -> {
"pdf"
}
"application/vnd.ms-excel" -> {
"xls"
}
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" -> {
"xlsx"
}
"application/vnd.ms-powerpoint" -> {
"ppt"
}
"application/vnd.openxmlformats-officedocument.presentationml.presentation" -> {
"pptx"
}
"text/plain" -> {
"txt"
}
else -> {
""
}
}
fileEntity.isDataFile = true
fileEntity.dirTypeStr = "data"
fileEntity.isDocumentFile = true
fileEntity.uriStr = file.uri.toString()
return fileEntity
}
}
TYPE_COMPRESS_TEXT -> {
if (fileType.endsWith(".zip") || fileType.endsWith(".rar") || fileType.endsWith(
".7z"
)
) {
val fileEntity = FileEntity(UI_TYPE_MAIN)
fileEntity.fileName = file.name
fileEntity.fileType = type
fileEntity.fileLastModifiedTime = TimeUtils.millis2String(
file.lastModified(),
SimpleDateFormat("dd/MM/yyyy")
)
fileEntity.fileLastModifiedTimeMils = file.lastModified()
fileEntity.fileSizeStr = ConvertUtils.byte2FitMemorySize(file.length())
fileEntity.fileSizeLong = file.length()
fileEntity.fileRealType = "rar"
fileEntity.isDataFile = true
fileEntity.dirTypeStr = "data"
fileEntity.isDocumentFile = true
fileEntity.uriStr = file.uri.toString()
return fileEntity
}
}
else -> {
}
}
}
return null
}
fun destroy() {
pathTextViewModel = null
fileScanViewModel = null
mainJob?.cancel()
mainJob = null
}
private fun getFileLastModifiedTime(paramFile: File): String? {
val calendar: Calendar = Calendar.getInstance()
val l = paramFile.lastModified()
val simpleDateFormat = SimpleDateFormat("dd/MM/yyyy")
calendar.timeInMillis = l
return simpleDateFormat.format(calendar.time)
}
private fun getDirTypeStrByPath(path: String): String {
return if (!path.contains("/"))
"other"
else {
val data =
path.split("/")
val str = data[data.size - 2]
str
}
}
}
PathTextViewModel------如果你需要在页面中显示扫描路径文案,那么就需要这个
kotlin
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.sugoilab.restore.bean.FileEntity
class PathTextViewModel : ViewModel() {
private val data: MutableLiveData<String> by lazy {
MutableLiveData<String>()
}
fun setData(path: String) {
this.data.postValue(path)
}
fun getData(): LiveData<String> {
return data
}
}
FileScanViewModel------文件扫描保存类的ViewModel,在这里对扫描到返回的FileEntity做判断和保存,并且把扫描到个文件个数通过Flow发射出去更新UI,内部逻辑可以按照自己需求处理,我这里需要对视频和音频文件单独获取文件时长,时长获取工具类顶部文章中有这里就不贴了。
kotlin
import android.text.TextUtils
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sugoilab.common.util.VideoUtil
import com.sugoilab.restore.MyApplication
import com.sugoilab.restore.bean.FileEntity
import com.sugoilab.restore.utill.TYPE_AUDIO_TEXT
import com.sugoilab.restore.utill.TYPE_VIDEO_TEXT
import com.sugoilab.restore.utill.millToTimeStr
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
class FileScanViewModel : ViewModel() {
// 对外暴露只读 StateFlow
private val _flow = MutableSharedFlow<Int>()
private val flow: SharedFlow<Int> = _flow.asSharedFlow()
fun setData(fileEntity: FileEntity) {
viewModelScope.launch {
//扫描到文件,更新文件列表
if (fileEntity.isDocumentFile) {
try {
if (fileEntity.fileSizeLong > 0) {
if (fileEntity.fileType == TYPE_VIDEO_TEXT || fileEntity.fileType == TYPE_AUDIO_TEXT) {
CoroutineScope(Dispatchers.IO).launch {
fileEntity.duration = fileEntity.uriStr?.let {
VideoUtil.getVideoOrAudioDurationTimeMill(
MyApplication.instance!!,
it,
true
)
}
fileEntity.durationStr = fileEntity.duration?.let {
millToTimeStr(
it
)
} ?: ""
}
}
MyApplication.scanFileList.add(fileEntity)
}
} catch (e: Exception) {
e.printStackTrace()
}
} else {
if (TextUtils.isEmpty(fileEntity.filePath))
return@launch
try {
if (fileEntity.fileSizeLong > 0 && !fileEntity.fileRealType.isNullOrEmpty()) {
if (fileEntity.fileType == TYPE_VIDEO_TEXT || fileEntity.fileType == TYPE_AUDIO_TEXT) {
CoroutineScope(Dispatchers.IO).launch {
fileEntity.duration = fileEntity.filePath?.let {
VideoUtil.getVideoOrAudioDurationTimeMill(
it
)
}
fileEntity.durationStr = fileEntity.duration?.let {
millToTimeStr(
it
)
} ?: ""
}
}
MyApplication.scanFileList.add(fileEntity)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
_flow.emit(MyApplication.scanFileList.size)
}
}
fun getFlow(): SharedFlow<Int> {
return flow
}
}
FileEntity文件实体类
kotlin
import android.graphics.Bitmap
import com.chad.library.adapter.base.entity.MultiItemEntity
import java.io.File
import java.io.Serializable
class FileEntity(override val itemType: Int) : Serializable, MultiItemEntity {
var fileLastModifiedTime: String? = null//文件上次修改时间的转换文案
var fileLastModifiedTimeMils: Long? = null//文件上次修改时间的毫秒
var fileName: String? = null//文件名
var filePath: String? = null//文件路径
var fileRealType: String? = null//文件真实类型
var fileSizeStr: String? = null//文件大小的转换文案
var fileSizeLong: Long = 0//文件大小的字节长度
var fileType: String? = null//文件类型-Utils的参数
var isDataFile = false//是否是android/data中的文件
var isCheck = false//是否被选中
var bitmap: Bitmap? = null//当前图片的bitmap文件
var recoverFilePath: String? = null
var newFileName: String? = null//处理后的文件名
var dirTypeStr = ""
var durationStr = "" //视频文件持续时长的格式化字符串
var duration: Long? = null //视频文件持续时长
var isDocumentFile = false //是否是通过documentfile方式获得的文件
var uriStr:String? = null //uri.toString后的数据,通过documentfile方式获得的文件必传
override fun toString(): String {
return "FileEntity(itemType=$itemType, fileLastModifiedTime=$fileLastModifiedTime, fileLastModifiedTimeMils=$fileLastModifiedTimeMils, fileName=$fileName, filePath=$filePath, fileRealType=$fileRealType, fileSizeStr=$fileSizeStr, fileSizeLong=$fileSizeLong, fileType=$fileType, isDataFile=$isDataFile, isCheck=$isCheck, bitmap=$bitmap, recoverFilePath=$recoverFilePath, newFileName=$newFileName, dirTypeStr='$dirTypeStr')"
}
}
CheckFileTypeUtil文件类型检查类
java
import android.text.TextUtils;
import com.sugoilab.restore.bean.FileTypeEntity;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CheckFileTypeUtil {
private static final List<FileTypeEntity> FILE_TYPE_ENTITY_LIST;
private static final HashMap<String, String> FILE_TYPE_MAP;
static {
HashMap<Object, Object> hashMap = new HashMap<Object, Object>();
FILE_TYPE_MAP = (HashMap) hashMap;
ArrayList<FileTypeEntity> arrayList = new ArrayList();
FILE_TYPE_ENTITY_LIST = arrayList;
hashMap.put("FFD8FF", "jpg");
hashMap.put("89504E", "png");
hashMap.put("47494638", "gif");
hashMap.put("1A45DFA3934282886D6174726F736B61", "mkv");
hashMap.put("00000020667479704D34412000000000", "m4a");
hashMap.put("000000146674797069736F6D", "mp4");
hashMap.put("00000018667479706D70343200000000", "mp4");//00000018667479706D70343200000000
hashMap.put("000000186674797033677035", "mp4");//00000018667479706D70343200000000
hashMap.put("0000001C66747970", "mp4");
hashMap.put("255044", "pdf");
hashMap.put("D0CF11E0", "doc");
hashMap.put("504B0304", "docx");
hashMap.put("D0CF11E0", "xls");
hashMap.put("504B0304", "xlsx");
hashMap.put("D0CF11E0", "ppt");
hashMap.put("504b0304", "pptx");
hashMap.put("49492A", "tif");
hashMap.put("424d22", "bmp");
hashMap.put("424d82", "bmp");
hashMap.put("424d8e", "bmp");
hashMap.put("414331", "dwg");
hashMap.put("3C2144", "htm");
hashMap.put("3C21444F4354", "html");
hashMap.put("48544d", "css");
hashMap.put("696b2e", "js");
hashMap.put("7B5C72", "rtf");
hashMap.put("384250", "psd");
hashMap.put("00000100", "ico");
hashMap.put("44656C69766572792D646174653A", "eml");
hashMap.put("5374616E64617264204A", "mdb");
hashMap.put("252150532D41646F6265", "ps");
hashMap.put("25215053", "eps");
hashMap.put("2E524D46", "rmvb");
hashMap.put("2E524D", "rm");
hashMap.put("464C5601", "flv");
hashMap.put("494433", "mp3");
hashMap.put("000001BA", "mpg");
hashMap.put("000001B3", "mpg");
hashMap.put("3026b2", "wmv");
hashMap.put("3026B2758E66CF11", "asf");
hashMap.put("57415645", "wav");
hashMap.put("41564920", "avi");
hashMap.put("4D546864", "mid");
hashMap.put("504B3030504B0304", "zip");
hashMap.put("52617221", "rar");
hashMap.put("235468", "ini");
hashMap.put("4d5a90", "exe");
hashMap.put("3c2540", "jsp");
hashMap.put("4d616e", "mf");
hashMap.put("3C3F786D6C", "xml");
hashMap.put("494e53", "sql");
hashMap.put("706163", "java");
hashMap.put("406563", "bat");
hashMap.put("1f8b", "gz");
hashMap.put("6c6f67", "properties");
hashMap.put("cafeba", "class");
hashMap.put("49545346", "chm");
hashMap.put("040000", "mxp");
hashMap.put("643130", "torrent");
hashMap.put("6D6F6F76", "mov");
hashMap.put("FF575043", "wpd");
hashMap.put("CFAD12FEC5FD746F", "dbx");
hashMap.put("2142444E", "pst");
hashMap.put("AC9EBD8F", "qdf");
hashMap.put("E3828596", "pwl");
hashMap.put("2E7261FD", "ram");
arrayList.clear();
for (Map.Entry<Object, Object> entry : hashMap.entrySet()) {
String str1 = (String) entry.getKey();
String str2 = (String) entry.getValue();
FileTypeEntity fileTypeEntity = new FileTypeEntity();
fileTypeEntity.setKey(str1);
fileTypeEntity.setValue(str2);
FILE_TYPE_ENTITY_LIST.add(fileTypeEntity);
}
}
private static String bytesToHexString(byte[] paramArrayOfbyte) {
StringBuilder stringBuilder = new StringBuilder();
if (paramArrayOfbyte == null || paramArrayOfbyte.length <= 0)
return null;
for (int i = 0; i < paramArrayOfbyte.length; i++) {
String str = Integer.toHexString(paramArrayOfbyte[i] & 0xFF).toUpperCase();
if (str.length() < 2)
stringBuilder.append(0);
stringBuilder.append(str);
}
return stringBuilder.toString();
}
public static String getFileHeader(String paramString) {
String str2 = null;
String str1 = str2;
try {
FileInputStream fileInputStream = new FileInputStream(paramString);
try {
byte[] arrayOfByte = new byte[16];
fileInputStream.read(arrayOfByte, 0, 16);
String str = bytesToHexString(arrayOfByte);
str1 = str;
return str;
} finally {
fileInputStream.close();
paramString = null;
}
} catch (IOException iOException) {
iOException.printStackTrace();
return str1;
}
}
public static String getFileType(String paramString) {
paramString = getFileHeader(paramString);
if (!TextUtils.isEmpty(paramString)) {
int i = 0;
while (true) {
List<FileTypeEntity> list = FILE_TYPE_ENTITY_LIST;
if (i < list.size()) {
String nowTypeHeader = list.get(i).getKey().toUpperCase();
if (paramString.length() >= nowTypeHeader.length()) {
//被识别的文件头长度大于当前循环类型的文件头长度,那么就判断被识别的文件头初始是否与当前循环类型文件头一样,如果一样则表示是指定类型文件
if (paramString.startsWith(nowTypeHeader)){
return FILE_TYPE_MAP.get(list.get(i).getKey());
}
}else {
//被识别的文件头长度小于当前循环类型的文件头长度,那么就判断当前循环类型的文件头初始是否与被识别文件头一样,如果一样则表示是指定类型文件
if (nowTypeHeader.startsWith(paramString)){
return FILE_TYPE_MAP.get(list.get(i).getKey());
}
}
// if (paramString.contains(list.get(i).getKey().toUpperCase()) || list.get(i).getKey().toUpperCase().contains(paramString))
// return FILE_TYPE_MAP.get(list.get(i).getKey());
i++;
continue;
}
break;
}
}
return "";
}
public static void main(String[] paramArrayOfString) {
String str = getFileType("D:\picture.jpg");
PrintStream printStream = System.out;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("fileType = ");
stringBuilder.append(str);
printStream.println(stringBuilder.toString());
}
}
3、开始扫描
kotlin
class FileScanningActivity :
BaseActivity<ActivityFileScanningBinding, BasePresenter<FileScanningActivity>>() {
...
private var scanUtil: ScanFileNewUtil? = null
override fun initData() {
val pathTextViewModel: PathTextViewModel by viewModels()
pathTextViewModel.getData().observe(this) {
//更新页面扫描路径显示
binding.tvPath.text = it
}
val fileScanViewModel: FileScanViewModel by viewModels()
MainScope().launch {
fileScanViewModel.getFlow().collect { size ->
//更新扫描的文件个数
binding.tvScanning.text =
"Scanning(${size})..."
}
}
scanUtil = ScanFileNewUtil(applicationContext, fileScanViewModel, pathTextViewModel)
scanFile()
}
private var scanningTime = 0L
private var scanJob:Job? = null
private fun scanFile() {
scanningTime = SystemClock.elapsedRealtime()
scanJob = CoroutineScope(Dispatchers.Default).launch {
if (mode == SCAN_TYPE_DEEP)
scanUtil?.getAllFilesDeep(type)
else
scanUtil?.getAllFilesNormal(type)
//按序执行,上面扫描完成后才会往下执行所以直接在这里写结束逻辑
//扫描完成,跳转
if (!isFinishing) {
LogUtils.e(
"扫描完成,扫描到数量:",
MyApplication.scanFileList.size,
"总扫描时长:",
TimeUtils.millis2String(
SystemClock.elapsedRealtime() - scanningTime,
"HH:mm:ss"
)
)
MyApplication.instance?.handleFileList()
//扫描完成,跳转
FileScanSuccessActivity.show(this@FileScanningActivity, type, mode)
finish()
}
}
}
override fun onDestroy() {
super.onDestroy()
scanJob?.cancel()
scanJob = null
scanUtil?.destroy()
scanUtil = null
}
}