# FileOperator **Repository Path**: q5938/FileOperator ## Basic Information - **Project Name**: FileOperator - **Description**: Android文件选择 & 存储管理框架 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2020-10-20 - **Last Updated**: 2022-05-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README > **上一篇** 👉 [Android Q & Android 11存储适配(一) 基础知识点梳理](https://juejin.im/post/6854573214447140871) # [FileOperator](https://github.com/javakam/FileOperator) - 🚀[FileOperator GitHub](https://github.com/javakam/FileOperator) - 🚀更简单的处理Android系统文件操作 - 🚀适用于 Android 4.4 及以上系统 , 兼容AndroidQ新的存储策略 - 🚀图片压缩算法参考 [Luban](https://github.com/Curzibn/Luban) - 🚀Kotlin 案例 👉 [app](https://github.com/javakam/FileOperator/tree/master/app) - 🚀Java 案例 👉 [sample_java](https://github.com/javakam/FileOperator/tree/master/sample_java) ## Gradle: Project `build.gradle` : ``` repositories { maven { url 'https://dl.bintray.com/javakam/maven' } } ``` > 推荐方式 : ``` implementation 'ando.file:core:1.0.0' //核心库必选 implementation 'ando.file:android-q:1.0.0' //AndroidQ & Android 11 兼容库 implementation 'ando.file:compressor:1.0.0' //图片压缩,核心算法采用 Luban implementation 'ando.file:selector:1.0.0' //文件选择器 ``` 整体引入(不推荐): ``` implementation 'ando.file:FileOperator:0.9.1' ``` `Application`中初始化: ``` FileOperator.init(this,BuildConfig.DEBUG) ``` ## 演示 | 功能列表 | 缓存目录 | |:---:|:---:| | | | ### API | App Specific | MediaStore | Storage Access Framework| |:---:|:---:|:---:| |![](https://raw.githubusercontent.com/javakam/FileOperator/master/screenshot/img2.png)|![](https://raw.githubusercontent.com/javakam/FileOperator/master/screenshot/img3.png)|![](https://raw.githubusercontent.com/javakam/FileOperator/master/screenshot/img4.png)| ### 文件选择 | 单图 + 压缩 | 多图 + 压缩 | 多文件 | |:---:|:---:|:---:| |![](https://raw.githubusercontent.com/javakam/FileOperator/master/screenshot/pick1.png)|![](https://raw.githubusercontent.com/javakam/FileOperator/master/screenshot/pick2.png)|![](https://raw.githubusercontent.com/javakam/FileOperator/master/screenshot/pick3.png)| ## Usage: ### 1. 单选图片 ``` val optionsImage = FileSelectOptions() optionsImage.fileType = FileType.IMAGE options.mMinCount = 0 options.mMaxCount = 10 optionsImage.mSingleFileMaxSize = 2097152 // 20M = 20971520 B optionsImage.mSingleFileMaxSizeTip = "图片最大不超过2M!" optionsImage.mAllFilesMaxSize = 5242880 //5M 5242880 ; 20M = 20971520 B optionsImage.mAllFilesMaxSizeTip = "总图片大小不超过5M!" optionsImage.mFileCondition = object : FileSelectCondition { override fun accept(fileType: FileType, uri: Uri?): Boolean { return (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri)) } } mFileSelector = FileSelector .with(this) .setRequestCode(REQUEST_CHOOSE_FILE) .setSelectMode(false) .setMinCount(1, "至少选一个文件!") .setMaxCount(10, "最多选十个文件!") .setSingleFileMaxSize(5242880, "大小不能超过5M!") //5M 5242880 ; 100M = 104857600 KB .setAllFilesMaxSize(10485760, "总大小不能超过10M!") .setMimeTypes(MIME_MEDIA)//默认全部文件, 不同 arrayOf("video/*","audio/*","image/*") 系统提供的选择UI不一样 .applyOptions(optionsImage) //优先使用 FileOptions 中设置的 FileSelectCondition .filter(object : FileSelectCondition { override fun accept(fileType: FileType, uri: Uri?): Boolean { return when (fileType) { FileType.IMAGE -> { return (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri)) } FileType.VIDEO -> true FileType.AUDIO -> true else -> true } } }) .callback(object : FileSelectCallBack { override fun onSuccess(results: List?) { FileLogger.w("回调 onSuccess ${results?.size}") mTvResult.text = "" if (results.isNullOrEmpty()) return shortToast("正在压缩图片...") showSelectResult(results) } override fun onError(e: Throwable?) { FileLogger.e("回调 onError ${e?.message}") mTvResultError.text = mTvResultError.text.toString().plus(" 错误信息: ${e?.message} \n") } }) .choose() ``` ### 2. 多选图片 ``` val optionsImage = FileSelectOptions() optionsImage.fileType = FileType.IMAGE options.mMinCount = 0 options.mMaxCount = 10 optionsImage.mSingleFileMaxSize = 3145728 // 20M = 20971520 B optionsImage.mSingleFileMaxSizeTip = "单张图片最大不超过3M!" optionsImage.mAllFilesMaxSize = 5242880 //3M 3145728 ; 5M 5242880 ; 10M 10485760 ; 20M = 20971520 B optionsImage.mAllFilesMaxSizeTip = "图片总大小不超过5M!" optionsImage.mFileCondition = object : FileSelectCondition { override fun accept(fileType: FileType, uri: Uri?): Boolean { return (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri)) } } mFileSelector = FileSelector .with(this) .setRequestCode(REQUEST_CHOOSE_FILE) .setSelectMode(true) .setMinCount(1, "至少选一个文件!") .setMaxCount(10, "最多选十个文件!") //优先以自定义的 optionsImage.mSingleFileMaxSize 为准5M 5242880 ; 100M = 104857600 KB .setSingleFileMaxSize(2097152, "大小不能超过2M!") .setAllFilesMaxSize(20971520, "总文件大小不能超过20M!") //1.OVER_SIZE_LIMIT_ALL_DONT 超过限制大小全部不返回 ;2.OVER_SIZE_LIMIT_EXCEPT_OVERFLOW_PART 超过限制大小去掉后面相同类型文件 .setOverSizeLimitStrategy(this.mOverSizeStrategy) .setMimeTypes(MIME_MEDIA)//默认全部文件, 不同 arrayOf("video/*","audio/*","image/*") 系统提供的选择UI不一样 .applyOptions(optionsImage) //优先使用 FileOptions 中设置的 FileSelectCondition .filter(object : FileSelectCondition { override fun accept(fileType: FileType, uri: Uri?): Boolean { return when (fileType) { FileType.IMAGE -> { return (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri)) } FileType.VIDEO -> true FileType.AUDIO -> true else -> true } } }) .callback(object : FileSelectCallBack { override fun onSuccess(results: List?) { FileLogger.w("回调 onSuccess ${results?.size}") mTvResult.text = "" if (results.isNullOrEmpty()) return shortToast("正在压缩图片...") showSelectResult(results) } override fun onError(e: Throwable?) { FileLogger.e("回调 onError ${e?.message}") mTvResultError.text = mTvResultError.text.toString().plus(" 错误信息: ${e?.message} \n") } }) .choose() ``` ### 3. 多选文件 > 🌴适用于处理复杂文件选择情形, 如: 选取图片、视频文件,其中图片至少选择一张, 最多选择两张, 每张图片大小不超过3M, 全部图片大小不超过5M ; 视频文件只能选择一个, 每个视频大小不超过20M, 全部视频大小不超过30M 。 ``` //图片 val optionsImage = FileSelectOptions().apply { fileType = FileType.IMAGE mMinCount = 1 mMaxCount = 2 mMinCountTip = "至少选择一张图片" mMaxCountTip = "最多选择两张图片" mSingleFileMaxSize = 3145728 // 20M = 20971520 B mSingleFileMaxSizeTip = "单张图片最大不超过3M!" mAllFilesMaxSize = 5242880 // 5M 5242880 mAllFilesMaxSizeTip = "图片总大小不超过5M!" mFileCondition = object : FileSelectCondition { override fun accept(fileType: FileType, uri: Uri?): Boolean { return (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri)) } } } //视频 val optionsVideo = FileSelectOptions().apply { fileType = FileType.VIDEO mMinCount = 1 mMaxCount = 1 mMinCountTip = "至少选择一个视频文件" mMaxCountTip = "最多选择一个视频文件" mSingleFileMaxSize = 20971520 // 20M = 20971520 B mSingleFileMaxSizeTip = "单视频最大不超过20M!" mAllFilesMaxSize = 31457280 //3M 3145728 mAllFilesMaxSizeTip = "视频总大小不超过30M!" mFileCondition = object : FileSelectCondition { override fun accept(fileType: FileType, uri: Uri?): Boolean { return (uri != null) } } } mFileSelector = FileSelector .with(this) .setRequestCode(REQUEST_CHOOSE_FILE) .setSelectMode(true) .setMinCount(1, "至少选一个文件!") .setMaxCount(5, "最多选五个文件!") // 优先使用自定义 FileSelectOptions 中设置的单文件大小限制,如果没有设置则采用该值 // 100M = 104857600 KB ;80M 83886080 ;50M 52428800 ; 20M 20971520 ;5M 5242880 ; .setSingleFileMaxSize(2097152, "单文件大小不能超过2M!") .setAllFilesMaxSize(52428800, "总文件大小不能超过50M!") // 超过限制大小两种返回策略: 1.OVER_SIZE_LIMIT_ALL_DONT,超过限制大小全部不返回;2.OVER_SIZE_LIMIT_EXCEPT_OVERFLOW_PART,超过限制大小去掉后面相同类型文件 .setOverSizeLimitStrategy(OVER_SIZE_LIMIT_EXCEPT_OVERFLOW_PART) .setMimeTypes(null)//默认为 null,*/* 即不做文件类型限定; MIME_MEDIA 媒体文件, 不同 arrayOf("video/*","audio/*","image/*") 系统提供的选择UI不一样 .applyOptions(optionsImage, optionsVideo) // 优先使用 FileOptions 中设置的 FileSelectCondition , 没有的情况下才使用通用的 .filter(object : FileSelectCondition { override fun accept(fileType: FileType, uri: Uri?): Boolean { return when (fileType) { FileType.IMAGE -> { return (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri)) } FileType.VIDEO -> true FileType.AUDIO -> true else -> true } } }) .callback(object : FileSelectCallBack { override fun onSuccess(results: List?) { FileLogger.w("回调 onSuccess ${results?.size}") mTvResult.text = "" if (results.isNullOrEmpty()) return showSelectResult(results) } override fun onError(e: Throwable?) { FileLogger.e("回调 onError ${e?.message}") mTvResultError.text = mTvResultError.text.toString().plus(" 错误信息: ${e?.message} \n") } }) .choose() ``` ### 4.压缩图片 [ImageCompressor.kt](https://github.com/javakam/FileOperator/blob/master/library_compressor/src/main/java/ando/file/compressor/ImageCompressor.kt) ``` //T 为 String.filePath / Uri / File fun compressImage(photos: List) { ImageCompressor .with(this) .load(photos) .ignoreBy(100)//B .setTargetDir(getPathImageCache()) .setFocusAlpha(false) .enableCache(true) .filter(object : ImageCompressPredicate { override fun apply(uri: Uri?): Boolean { //getFilePathByUri(uri) FileLogger.i("image predicate $uri ${getFilePathByUri(uri)}") return if (uri != null) { val path = getFilePathByUri(uri) !(TextUtils.isEmpty(path) || (path?.toLowerCase() ?.endsWith(".gif") == true)) } else { false } } }) .setRenameListener(object : OnImageRenameListener { override fun rename(uri: Uri?): String? { try { val filePath = getFilePathByUri(uri) val md = MessageDigest.getInstance("MD5") md.update(filePath?.toByteArray() ?: return "") return BigInteger(1, md.digest()).toString(32) } catch (e: NoSuchAlgorithmException) { e.printStackTrace() } return "" } }) .setImageCompressListener(object : OnImageCompressListener { override fun onStart() {} override fun onSuccess(uri: Uri?) { val path = "$cacheDir/image/" FileLogger.i("compress onSuccess uri=$uri path=${uri?.path} 缓存目录总大小=${FileSizeUtils.getFolderSize(File(path))}") val bitmap = getBitmapFromUri(uri) dumpMetaData(uri) { displayName: String?, size: String? -> runOnUiThread { mTvResult.text = mTvResult.text.toString().plus( "\n ---------\n👉压缩后 \n Uri : $uri \n 路径: ${uri?.path} \n 文件名称 :$displayName \n 大小:$size B \n" + "格式化 : ${FileSizeUtils.formatFileSize(size?.toLong() ?: 0L)}\n ---------" ) } } mIvCompressed.setImageBitmap(bitmap) } override fun onError(e: Throwable?) { FileLogger.e("compress onError ${e?.message}") } }).launch() } ``` ## 直接使用静态方法 ### 1. 获取文件MimeType类型👉[FileMimeType.kt](https://github.com/javakam/FileOperator/blob/master/library/src/main/java/ando/file/core/FileMimeType.kt) #### 根据`File Name/Path/Url`获取相应`MimeType` ``` fun getMimeType(str: String?): String {...} fun getMimeType(uri: Uri?): String {...} //MimeTypeMap.getSingleton().getMimeTypeFromExtension(...) 的补充 fun getMimeTypeSupplement(fileName: String): String {...} ``` ### 2. 计算文件或文件夹的大小👉[FileSizeUtils.kt](https://github.com/javakam/FileOperator/blob/master/library/src/main/java/ando/file/core/FileSizeUtils.kt) #### 获取指定`文件/文件夹`大小 ``` @Throws(Exception::class) fun getFolderSize(file: File?): Long { var size = 0L if (file == null || !file.exists()) return size val files = file.listFiles() if (files.isNullOrEmpty()) return size for (i in files.indices) { size += if (files[i].isDirectory) getFolderSize(files[i]) else getFileSize(files[i]) } return size } ``` #### 获取文件大小 ``` fun getFileSize(file: File?): Long{...} fun getFileSize(uri: Uri?): Long{...} ``` #### 自动计算指定`文件/文件夹`大小 自动计算指定文件或指定文件夹的大小 , 返回值带 B、KB、M、GB、TB 单位的字符串 ``` fun getFileOrDirSizeFormatted(path: String?): String {}...} ``` #### 格式化大小(`BigDecimal`实现) ``` //scale 表示 精确到小数点以后几位 fun formatFileSize(size: Long, scale: Int): String {...} ``` 转换文件大小,指定转换的类型: ``` //scale 精确到小数点以后几位 fun formatSizeByType(size: Long, scale: Int, sizeType: FileSizeType): BigDecimal = BigDecimal(size.toDouble()).divide( BigDecimal( when (sizeType) { SIZE_TYPE_B -> 1L SIZE_TYPE_KB -> 1024L SIZE_TYPE_MB -> 1024L * 1024L SIZE_TYPE_GB -> 1024L * 1024L * 1024L SIZE_TYPE_TB -> 1024L * 1024L * 1024L * 1024L } ), scale, if (sizeType == SIZE_TYPE_B) BigDecimal.ROUND_DOWN else BigDecimal.ROUND_HALF_UP ) ``` 转换文件大小带单位: ``` fun getFormattedSizeByType(size: Long, scale: Int, sizeType: FileSizeType): String { return "${formatSizeByType(size, scale, sizeType).toPlainString()}${sizeType.unit}" } ``` ### 3. 直接打开Url/Uri(远程or本地)👉[FileOpener.kt](https://github.com/javakam/FileOperator/blob/master/library/src/main/java/ando/file/core/FileOpener.kt) #### 直接打开`Url`对应的系统应用 eg: 如果url是视频地址,则直接用系统的播放器打开 ``` fun openUrl(activity: Activity, url: String?) { try { val intent = Intent(Intent.ACTION_VIEW) intent.setDataAndType(Uri.parse(url), getMimeType(url)) activity.startActivity(intent) } catch (e: Exception) { FileLogger.e("openUrl error : " + e.message) } } ``` #### 根据 文件路径 和 类型(后缀判断) 显示支持该格式的程序 ``` fun openFileBySystemChooser(context: Any, uri: Uri?, mimeType: String? = null) = uri?.let { u -> Intent.createChooser(createOpenFileIntent(u, mimeType), "选择程序")?.let { startActivity(context, it) } } ``` #### 选择文件【调用系统的文件管理】 ``` fun createChooseIntent(mimeType: String?, mimeTypes: Array?, multiSelect: Boolean): Intent = // Implicitly allow the user to select a particular kind of data. Same as : Intent.ACTION_GET_CONTENT Intent(Intent.ACTION_OPEN_DOCUMENT).apply { putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiSelect) // The MIME data type filter //intent.setType("image/*"); //选择图片 //intent.setType("audio/*"); //选择音频 //intent.setType("video/*"); //选择视频 (mp4 3gp 是 android支持的视频格式) //intent.setType("file/*"); //比 */* 少了一些侧边栏选项 //intent.setType("video/*;image/*");//错误方式;同时选择视频和图片 -> https://www.jianshu.com/p/e98c97669af0 if (mimeType.isNullOrBlank() && mimeTypes.isNullOrEmpty()) type = "*/*" else { type = if (mimeType.isNullOrEmpty()) "*/*" else mimeType putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) } // Only return URIs that can be opened with ContentResolver addCategory(Intent.CATEGORY_OPENABLE) } ``` > 注:
    1.Intent.setType 不能为空!
    2.mimeTypes 会覆盖 mimeType
    3.ACTION_GET_CONTENT , ACTION_OPEN_DOCUMENT 效果相同
    4.开启多选 resultCode=-1 ### 4. 获取文件Uri/Path👉[FileUri.kt](https://github.com/javakam/FileOperator/blob/master/library/src/main/java/ando/file/core/FileUri.kt) #### 从`File`路径中获取`Uri` ``` fun getUriByPath(path: String?): Uri? = if (path.isNullOrBlank()) null else getUriByFile(File(path)) fun getUriByFile(file: File?): Uri? { if (file == null) return null return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val authority = FileOperator.getContext().packageName + PATH_SUFFIX FileProvider.getUriForFile(FileOperator.getContext(), authority, file) } else { Uri.fromFile(file) } } ``` #### 获取`Uri`对应的文件路径,兼容`API 26` ``` fun getFilePathByUri(context: Context?, uri: Uri?): String? { if (context == null || uri == null) return null val scheme = uri.scheme // 以 file:// 开头的 if (ContentResolver.SCHEME_FILE.equals(scheme, ignoreCase = true)) {//使用第三方应用打开 uri.path } return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { //4.4以后 getPath(context, uri) } else { //4.4以下 getPathKitkat(context, uri) } } ``` ### 5. 通用文件工具类👉[FileUtils.kt](https://raw.githubusercontent.com/javakam/FileOperator/master/library/src/main/java/com/ando/file/common/FileUtils.kt) - getExtension 获取文件后缀 `jpg` - getExtensionFull 获取文件后缀 `.jpg` - getExtensionFromUri(uri: Uri?) 获取文件后缀 - deleteFile 删除文件或目录 - deleteFilesButDir(file: File?, vararg excludeDirs: String?) 删除文件或目录 , `excludeDirs` 跳过指定名称的一些`目录/文件` - deleteFileDir 只删除文件,不删除文件夹 - readFileText 读取文本文件中的内容 `String` - readFileBytes 读取文本文件中的内容 `ByteArray` - copyFile 根据文件路径拷贝文件 `java.nio` ``` eg :boolean copyFile = FileUtils.copyFile(fileOld, "/test_" + i, getExternalFilesDir(null).getPath()); File fileNew =new File( getExternalFilesDir(null).getPath() +"/"+ "test_" + i); ``` - write2File(bitmap: Bitmap, fileName: String?) - write2File(input: InputStream?, filePath: String?) - isLocal 检验是否为本地URI - isGif 检验是否为 gif ## 注意的点 1. `onActivityResult` 中要把选择文件的结果交给`FileSelector`处理`mFileSelector?.obtainResult(requestCode, resultCode, data)` 2. 选择文件不满足预设条件时,有两种策略 : - 1.当设置总文件大小限制时,有两种策略 OVER_SIZE_LIMIT_ALL_DONT 只要有一个文件超出直接返回 onError - 2.OVER_SIZE_LIMIT_EXCEPT_OVERFLOW_PART 去掉超过限制大小的溢出部分的文件 3. 选择文件数据:单选 Intent.getData ; 多选 Intent.getClipData 4. Android 系统问题 : Intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) 开启多选条件下只选择一个文件时,需要安装单选逻辑走... Σ( ° △ °|||)︴ 5. 回调处理 多选模式下,建议使用统一的 CallBack 回调;
单选模式下,如果配置了自定义的 CallBack , 则优先使用该回调;否则使用统一的 CallBack ## 未来任务 ``` 1.做一个自定义UI的文件管理器 2.增加Fragment使用案例 , 视频压缩-郭笑醒 , 清除缓存功能 , 外置存储适配 3.整理更详细的文档 配合 com.liulishuo.okdownload 做文件下载 👉 library_file_downloader 4. ``` --- ## 参考 - Google 1. [Storage Samples Repository](https://github.com/android/storage-samples) 2. [SAF 使用存储访问框架打开文件](https://developer.android.google.cn/guide/topics/providers/document-provider) 3. [SAF API UseCase](https://developer.android.google.cn/training/data-storage/shared/documents-files) [管理分区外部存储访问](https://developer.android.google.cn/training/data-storage/files/external-scoped) [管理分区外部存储访问 - 如何从原生代码访问媒体文件 & MediaStore增删该查API](https://developer.android.google.cn/training/data-storage/shared/media) [处理外部存储中的媒体文件](https://developer.android.google.cn/training/data-storage/files/media) [Android 11 中的隐私权](https://developer.android.google.cn/preview/privacy) [Android 10 中的隐私权](https://developer.android.google.cn/about/versions/10/privacy/changes#scoped-storage) - Other [github/scoped_storage_sample](https://github.com/songlongGithub/scoped_storage_sample) [掘金-Android Q 适配指南 让你少走一堆弯路](https://juejin.im/post/5cad5b7ce51d456e5a0728b0) [Android Q 沙箱适配多媒体文件总结](https://segmentfault.com/a/1190000019224425) [oppo AndroidQ适配指导](https://open.oppomobile.com/wiki/doc#id=10432) [huawei Google Q版本应用兼容性整改指导](https://developer.huawei.com/consumer/cn/doc/50127) - 参考项目 [MaterialFiles](https://github.com/zhanghai/MaterialFiles) [Shelter](https://github.com/PeterCxy/Shelter) [FileUtils](https://github.com/coltoscosmin/FileUtils/blob/master/FileUtils.java) [cloud-player-android-sdk](https://github.com/codeages/cloud-player-android-sdk/blob/master/app/src/main/java/com/edusoho/playerdemo/util/FileUtils.java) ## library_file_downloader > 项目基于 [OkDownload](https://github.com/lingochamp/okdownload) 实现 - 断点异常的BUG - 中文文档 - Simple - Advanced - AndroidFilePicker - FilePicker ## bintrayUpload [novoda](ttps://github.com/novoda/bintray-release) `gradlew clean build bintrayUpload -PbintrayUser=javakam -PbintrayKey=xxx -PdryRun=false`