Android Q & Android 11存储适配(二) FileOperator文件管理框架

android ExifInterface获取图片信息与赋值于图片 Android 图片信息与修改 阅读详情

上一篇 👉 Android Q & Android 11存储适配(一) 基础知识点梳理

FileOperator

  • 🚀Android文件操作库
  • 🚀适用于Android 4.4及以上系统, 已兼容AndroidQ和Android11新的存储策略
  • 🚀Kotlin和Java案例 👉 app & sample_java

使用(Usage)

1. Project build.gradle:
repositories {
    maven { url 'https://dl.bintray.com/javakam/FileOperator' }
}
2. 依赖(dependencies)
implementation 'ando.file:core:1.3.8'       //核心库必选(Core library required)
implementation 'ando.file:selector:1.3.8'   //文件选择器(File selector)
implementation 'ando.file:compressor:1.3.8' //图片压缩, 核心算法采用 Luban
implementation 'ando.file:android-q:1.3.8'  //Q和11兼容库,需要额外的库:'androidx.documentfile:documentfile:1.0.1'
3. Application中初始化(Initialization in Application)
FileOperator.init(this,BuildConfig.DEBUG)
4. 混淆(Proguard)

未用到反射, 不需要混淆。(No reflection is used, no need to be confused.)

预览(Preview)

功能列表(Function list)缓存目录(Cache directory)

文件选择(File selection)

单图+压缩(Single Image+Compress)多图+压缩(Multiple images+Compress)多文件+多类型(Multiple files+Multiple types)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ea1lNmng-1615774263638)(https://raw.githubusercontent.com/javakam/FileOperator/master/screenshot/pick1.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FD2qhkGC-1615774263639)(https://raw.githubusercontent.com/javakam/FileOperator/master/screenshot/pick2.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F8kEZuez-1615774263641)(https://raw.githubusercontent.com/javakam/FileOperator/master/screenshot/pick3.png)]

用法(Usage)

1. 单选图片(Single selection picture)

val optionsImage = FileSelectOptions().apply {
    fileType = FileType.IMAGE
    fileTypeMismatchTip = "文件类型不匹配 !" //File type mismatch
    singleFileMaxSize = 5242880
    singleFileMaxSizeTip = "图片最大不超过5M !" //The largest picture does not exceed 5M
    allFilesMaxSize = 10485760
    allFilesMaxSizeTip = "总图片大小不超过10M !"//The total picture size does not exceed 10M  注:单选条件下无效,只做单个图片大小判断
    fileCondition = object : FileSelectCondition {
        override fun accept(fileType: IFileType, uri: Uri?): Boolean {
            return (fileType == FileType.IMAGE && uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri))
        }
    }
}
mFileSelector = FileSelector
    .with(this)
    .setRequestCode(REQUEST_CHOOSE_FILE)
    .setTypeMismatchTip("文件类型不匹配 !") //File type mismatch
    .setMinCount(1, "至少选择一个文件 !") //Choose at least one file
    .setMaxCount(10, "最多选择十个文件 !") //Choose up to ten files  注:单选条件下无效, 只做最少数量判断
    .setOverLimitStrategy(OVER_LIMIT_EXCEPT_OVERFLOW)
    .setSingleFileMaxSize(1048576, "大小不能超过1M !") //The size cannot exceed 1M  注:单选条件下无效, FileSelectOptions.singleFileMaxSize
    .setAllFilesMaxSize(10485760, "总大小不能超过10M !") //The total size cannot exceed 10M 注:单选条件下无效,只做单个图片大小判断 setSingleFileMaxSize
    .setMimeTypes("image/*") //默认不做文件类型约束为"*/*",不同类型系统提供的选择UI不一样 eg:"video/*","audio/*","image/*"
    .applyOptions(optionsImage)
    .filter(object : FileSelectCondition {
        override fun accept(fileType: IFileType, uri: Uri?): Boolean {
            return when (fileType) {
                FileType.IMAGE -> (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri))
                FileType.VIDEO -> false
                FileType.AUDIO -> false
                else -> false
            }
        }
    })
    .callback(object : FileSelectCallBack {
        override fun onSuccess(results: List<FileSelectResult>?) {
            ResultUtils.resetUI(mTvResult)
            if (results.isNullOrEmpty()) {
                toastLong("没有选取文件") //No file selected
                return
            }
            showSelectResult(results)
        }
        override fun onError(e: Throwable?) {
            FileLogger.e("FileSelectCallBack onError ${e?.message}")
            ResultUtils.setErrorText(mTvError, e)
        }
    })
    .choose()

2. 多选图片(多选+单一类型)

Multiple selection pictures (multiple selection + single type)

val optionsImage = FileSelectOptions().apply {
    fileType = FileType.IMAGE
    fileTypeMismatchTip = "文件类型不匹配 !" //File type mismatch
    singleFileMaxSize = 5242880
    singleFileMaxSizeTip = "单张图片大小不超过5M !" //The size of a single picture does not exceed 5M
    allFilesMaxSize = 10485760
    allFilesMaxSizeTip = "图片总大小不超过10M !" //The total size of the picture does not exceed 10M
    fileCondition = object : FileSelectCondition {
        override fun accept(fileType: IFileType, uri: Uri?): Boolean {
            return (fileType == FileType.IMAGE && uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri))
        }
    }
}
mFileSelector = FileSelector
    .with(this)
    .setRequestCode(REQUEST_CHOOSE_FILE)
    .setMultiSelect()
    .setMinCount(1, "至少选择一个文件 !") //Choose at least one file
    .setMaxCount(2, "最多选两个文件!") //Choose up to two files
    .setSingleFileMaxSize(3145728, "单个大小不能超过3M !") //Single size cannot exceed 3M
    .setAllFilesMaxSize(20971520, "总文件大小不能超过20M !") //The total file size cannot exceed 20M
    .setOverLimitStrategy(this.mOverLimitStrategy)
    .setMimeTypes("image/*")
    .applyOptions(optionsImage)
    .filter(object : FileSelectCondition {
        override fun accept(fileType: IFileType, uri: Uri?): Boolean {
            return (fileType == FileType.IMAGE) && (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri))
        }
    })
    .callback(object : FileSelectCallBack {
        override fun onSuccess(results: List<FileSelectResult>?) {
            FileLogger.w("FileSelectCallBack onSuccess ${results?.size}")
            mAdapter.setData(null)
            if (results.isNullOrEmpty()) {
                toastLong("没有选取文件") //No file selected
                return
            }
            showSelectResult(results)
        }
        override fun onError(e: Throwable?) {
            FileLogger.e("FileSelectCallBack onError ${e?.message}")
            ResultUtils.setErrorText(mTvError, e)
            mAdapter.setData(null)
            mBtSelect.text = "$mShowText (0)"
        }
    })
    .choose()

3. 多选文件(多选+多种类型)

Multiple files (multi-select multiple types)

🌴适用于处理复杂文件选择情形, 如: 选取图片、音频文件、文本文件, 其中图片至少选择一张, 最多选择两张, 每张图片大小不超过5M, 全部图片大小不超过10M;
音频文件至少选择两个, 最多选择三个, 每个音频大小不超过20M, 全部音频大小不超过30M;
文本文件至少选择一个, 最多选择两个, 每个文本文件大小不超过5M, 全部文本文件大小不超过10M

🌴It is suitable for processing complex file selection situations, such as: select pictures, audio files, text files, among which, select at least one picture and two at most. The size of each picture does not exceed 5M, and the size of all pictures does not exceed 10M; audio FileChoose at least two and a maximum of three, each audio size does not exceed 20M, all audio size does not exceed 30M; text file select at least one, select at most two, each text file size does not exceed 5M, all The text file size does not exceed 10M

//图片 Image
val optionsImage = FileSelectOptions().apply {
    fileType = FileType.IMAGE
    minCount = 1
    maxCount = 2
    minCountTip = "至少选择一张图片" //Select at least one picture
    maxCountTip = "最多选择两张图片" //Select up to two pictures
    singleFileMaxSize = 5242880
    singleFileMaxSizeTip = "单张图片最大不超过5M !" //A single picture does not exceed 5M !
    allFilesMaxSize = 10485760
    allFilesMaxSizeTip = "图片总大小不超过10M !" //The total size of the picture does not exceed 10M !
    fileCondition = object : FileSelectCondition {
        override fun accept(fileType: IFileType, uri: Uri?): Boolean {
            return (fileType == FileType.IMAGE && uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri))
        }
    }
}
//音频 Audio
val optionsAudio = FileSelectOptions().apply {
    fileType = FileType.AUDIO
    minCount = 2
    maxCount = 3
    minCountTip = "至少选择两个音频文件" //Select at least two audio files
    maxCountTip = "最多选择三个音频文件" //Select up to three audio files
    singleFileMaxSize = 20971520
    singleFileMaxSizeTip = "单音频最大不超过20M !" //Maximum single audio does not exceed 20M !
    allFilesMaxSize = 31457280
    allFilesMaxSizeTip = "音频总大小不超过30M !" //The total audio size does not exceed 30M !
    fileCondition = object : FileSelectCondition {
        override fun accept(fileType: IFileType, uri: Uri?): Boolean {
            return (uri != null)
        }
    }
}
//文本文件 txt
val optionsTxt = FileSelectOptions().apply {
    fileType = FileType.TXT
    minCount = 1
    maxCount = 2
    minCountTip = "至少选择一个文本文件" //Select at least one text file
    maxCountTip = "最多选择两个文本文件" //Select at most two text files
    singleFileMaxSize = 5242880
    singleFileMaxSizeTip = "单文本文件最大不超过5M !" //The single biggest text file no more than 5M
    allFilesMaxSize = 10485760
    allFilesMaxSizeTip = "文本文件总大小不超过10M !" //Total size not more than 10M text file
    fileCondition = object : FileSelectCondition {
        override fun accept(fileType: IFileType, uri: Uri?): Boolean {
            return (uri != null)
        }
    }
}
/*
 注:如果某个FileSelectOptions没通过限定条件, 则该FileSelectOptions不会返回
 eg: 采用上面的限制条件下,图片、音频、文本文件各选一个, 因为音频最小数量设定为`2`不满足设定条件则去除所有音频选择结果
    , 所以返回结果中只有图片和文本文件(限于OVER_LIMIT_EXCEPT_OVERFLOW)

 EN:
 Note: if a FileSelectOptions failed the qualification, then the FileSelectOptions will not return,
 Eg: using the restriction conditions, images, audio, text files, each choose a, because audio set the minimum amount as ` 2 ` set does not meet the conditions the choice of the results to remove all audio
    , Only pictures and text files, so return result (limited to OVER_LIMIT_EXCEPT_OVERFLOW);
 */
mFileSelector = FileSelector
    .with(this)
    .setRequestCode(REQUEST_CHOOSE_FILE)
    .setMultiSelect() //默认是单选false (The default is radio false)

    /*
    实际最少数量限制为 setMinCount 和 (optionsImage.minCount + optionsAudio.minCount +...) 中的最小值
    实际最大数量限制为 setMaxCount 和 (optionsImage.maxCount + optionsAudio.maxCount +...) 中的最大值, 所以此处的最大值限制是无效的
    EN:
    Actual minimum limit for setMinCount and (optionsImage minCount optionsAudio. MinCount... The lowest value of),
    Actual maximum limit for setMaxCount and (optionsImage maxCount optionsAudio. MaxCount... ) the maximum, so the maximum limit here is invalid;
     */
    .setMinCount(1, "设定类型文件至少选择一个!") //Select at least one set type file
    .setMaxCount(4, "最多选四个文件!") //Most alternative four files

    /*
    实际单文件大小限制为 setSingleFileMaxSize 和 (optionsImage.singleFileMaxSize + optionsAudio.singleFileMaxSize +...) 中的最小值
    实际总大小限制为 setAllFilesMaxSize 和 (optionsImage.allFilesMaxSize + optionsAudio.allFilesMaxSize +...) 中的最大值
    EN:
    Actual single file size limit for setSingleFileMaxSize and (optionsImage. SingleFileMaxSize optionsAudio. SingleFileMaxSize... The lowest value of),
    Actual total size limit for setAllFilesMaxSize and (optionsImage allFilesMaxSize optionsAudio. AllFilesMaxSize... The highest value in);
     */
    //优先使用 `自定义FileSelectOptions` 中设置的单文件大小限制, 如果没有设置则采用该值
    //EN:Prefer using ` custom FileSelectOptions ` set in single file size limit, if the value is not set is used
    .setSingleFileMaxSize(2097152, "单文件大小不能超过2M !") //The size of a single file cannot exceed 2M !
    .setAllFilesMaxSize(52428800, "总文件大小不能超过50M !") //The total file size cannot exceed 50M !

    //1. 文件超过数量限制或大小限制
    //2. 单一类型: 保留未超限制的文件并返回, 去掉后面溢出的部分; 多种类型: 保留正确的文件, 去掉错误类型的所有文件
    //EN:
    //1. Documents more than limit or size limit
    //2. Single type: keep not ultra limit file and return, get rid of the overflow part; Multiple types: keep the right file, get rid of the wrong type of all documents
    .setOverLimitStrategy(this.mOverLimitStrategy)

    //eg: ando.file.core.FileMimeType
    //默认不做文件类型约束为"*/*", 不同类型系统提供的选择UI不一样 eg: "video/*","audio/*","image/*"
    //EN:Default do not file type constraints for "/", is not the same as the choice of different types of the system to provide the UI eg: "video/"," audio/", "image/"
    .setMimeTypes("audio/*", "image/*", "text/plain")

    //如果setMimeTypes和applyOptions没对应上会出现`文件类型不匹配问题`
    //EN:If setMimeTypes and applyOptions no corresponding will appear `file type mismatch problems`
    .applyOptions(optionsImage, optionsAudio, optionsTxt)

    //优先使用 FileSelectOptions 中设置的 FileSelectCondition
    //EN:Priority in use FileSelectOptions FileSelectCondition Settings
    .filter(object : FileSelectCondition {
        override fun accept(fileType: IFileType, uri: Uri?): Boolean {
            return when (fileType) {
                FileType.IMAGE -> (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri))
                FileType.AUDIO -> true
                FileType.TXT -> true
                else -> false
            }
        }
    })
    .callback(object : FileSelectCallBack {
        override fun onSuccess(results: List<FileSelectResult>?) {
            FileLogger.w("FileSelectCallBack onSuccess ${results?.size}")
            mAdapter.setData(null)
            if (results.isNullOrEmpty()) {
                toastLong("没有选取文件") //No files selected
                return
            }
            showSelectResult(results)
        }
        override fun onError(e: Throwable?) {
            FileLogger.e("FileSelectCallBack onError ${e?.message}")
            ResultUtils.setErrorText(mTvError, e)
            mAdapter.setData(null)
            mBtSelect.text = "$mShowText (0)"
        }
    })
    .choose()

4. 自定义FileType(Custom FileType)

①扩展已有的FileType

Extend existing FileType

eg: 
内置(built in): TXT(mutableListOf("txt", "conf", "iml", "ini", "log", "prop", "rc"))

增加(increase): FileType.TXT.supplement("gradle","kt")
结果(result): TXT(mutableListOf("txt", "conf", "iml", "ini", "log", "prop", "rc","gradle","kt"))

移除(remove): FileType.TXT.remove("txt","ini")
结果(result): TXT(mutableListOf("conf", "iml", log", "prop", "rc"))

替换(replace): FileType.XML.replace("xxx")
调试(debugging): FileType.TXT.dump()
②通过IFileType自定义文件类型

Through IFileType custom file type

🍎下面提供了两种实现的方式 (The following provides two ways):

//1.方式一
object FileTypePhp : IFileType {
    override fun fromUri(uri: Uri?): IFileType {
        return if (parseSuffix(uri).equals("php", true)) FileTypePhp else FileType.UNKNOWN
    }
}
//2.推荐方式 (Recommended way)
enum class FileTypeJson : IFileType {
    JSON;
    override fun fromUri(uri: Uri?): IFileType {
        return resolveFileMatch(uri, "json", JSON)
    }
}

用法(Usage) :

val optionsJsonFile = FileSelectOptions().apply {
    fileType = FileTypeJson.JSON
    minCount = 1
    maxCount = 2
    minCountTip = "至少选择一个JSON文件" //Choose at least one JSON file
    maxCountTip = "最多选择两个JSON文件" //Choose up to two JSON files
}

FileSelector.with(this)
    ...
    .setMimeTypes("audio/*", "image/*", "text/*", "application/json")
    .applyOptions(optionsImage, optionsAudio, optionsTxt, optionsJsonFile)
    .filter(object : FileSelectCondition {
        override fun accept(fileType: IFileType, uri: Uri?): Boolean {
            return when (fileType) {
                FileType.IMAGE -> (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri))
                FileType.AUDIO -> true
                FileType.TXT -> true
                FileTypeJson.JSON -> true
                else -> false
            }
        }
    })
    .choose()

注意: json文件无法用text/*打开, 对应的mimeType为application/json

5. 压缩图片 ImageCompressor.kt

方式一 直接压缩不缓存(Direct compression without caching)
val bitmap:Bitmap=ImageCompressEngine.compressPure(uri)
方式二 压缩图片并缓存(Compress pictures and cache)
/**
 * 压缩图片 1.Luban算法; 2.直接压缩 -> val bitmap:Bitmap=ImageCompressEngine.compressPure(uri)
 *
 * T : String.filePath / Uri / File
 */
fun <T> compressImage(context: Context, photos: List<T>, success: (index: Int, uri: Uri?) -> Unit) {
    ImageCompressor
        .with(context)
        .load(photos)
        .ignoreBy(100)//Byte
        .setTargetDir(getCompressedImageCacheDir())
        .setFocusAlpha(false)
        .enableCache(true)
        .filter(object : ImageCompressPredicate {
            override fun apply(uri: Uri?): Boolean {
                //FileLogger.i("compressImage predicate $uri  ${FileUri.getFilePathByUri(uri)}")
                return if (uri != null) !FileUtils.getExtension(uri).endsWith("gif") else false
            }
        })
        .setRenameListener(object : OnImageRenameListener {
            override fun rename(uri: Uri?): String {
                try {
                    val filePath = FileUri.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(index: Int, uri: Uri?) {
                success.invoke(index, uri)
            }

            override fun onError(e: Throwable?) {
                FileLogger.e("OnImageCompressListener onError ${e?.message}")
            }
        }).launch()
}

文件操作工具类(File operation tools)

☘ FileOperator提供了Android开发常用的一些文件操作工具类,使用方式大多以静态方法为主,需要的同学可以直接CV需要的文件

1. 获取文件MimeType类型👉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

获取指定文件/文件夹大小(Get the size of the specified file folder)
@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
}
获取文件大小(Get file size)
fun getFileSize(file: File?): Long{...}

fun getFileSize(uri: Uri?): Long{...}
自动计算指定文件/文件夹大小(Automatically calculate the size of the specified file folder)

自动计算指定文件或指定文件夹的大小 , 返回值带 B、KB、M、GB、TB 单位的字符串

fun getFileOrDirSizeFormatted(path: String?): String {}...}
格式化大小(BigDecimal实现)

Format size (implemented by Big Decimal)

/**
 * @param scale 精确到小数点以后几位 (Accurate to a few decimal places)
 */
fun formatFileSize(size: Long, scale: Int, withUnit: Boolean = false): String {
    val divisor = 1024L
    //ROUND_DOWN 1023 -> 1023B ; ROUND_HALF_UP  1023 -> 1KB
    val kiloByte: BigDecimal = formatSizeByTypeWithDivisor(BigDecimal.valueOf(size), scale, SIZE_TYPE_B, divisor)
    if (kiloByte.toDouble() < 1) {
        return "${kiloByte.toPlainString()}${if (withUnit) SIZE_TYPE_B.unit else ""}"
    }
    //KB
    val megaByte = formatSizeByTypeWithDivisor(kiloByte, scale, SIZE_TYPE_KB, divisor)
    if (megaByte.toDouble() < 1) {
        return "${kiloByte.toPlainString()}${if (withUnit) SIZE_TYPE_KB.unit else ""}"
    }
    //M
    val gigaByte = formatSizeByTypeWithDivisor(megaByte, scale, SIZE_TYPE_MB, divisor)
    if (gigaByte.toDouble() < 1) {
        return "${megaByte.toPlainString()}${if (withUnit) SIZE_TYPE_MB.unit else ""}"
    }
    //GB
    val teraBytes = formatSizeByTypeWithDivisor(gigaByte, scale, SIZE_TYPE_GB, divisor)
    if (teraBytes.toDouble() < 1) {
        return "${gigaByte.toPlainString()}${if (withUnit) SIZE_TYPE_GB.unit else ""}"
    }
    //TB
    return "${teraBytes.toPlainString()}${if (withUnit) SIZE_TYPE_TB.unit else ""}"
}

转换文件大小,指定转换的类型(Convert file size, specify the type of conversion):

//scale 精确到小数点以后几位
fun formatSizeByTypeWithoutUnit(size: BigDecimal, scale: Int, sizeType: FileSizeType): BigDecimal =
    size.divide(
        BigDecimal.valueOf(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,
        //ROUND_DOWN 1023 -> 1023B ; ROUND_HALF_UP  1023 -> 1KB
        if (sizeType == SIZE_TYPE_B) BigDecimal.ROUND_DOWN else BigDecimal.ROUND_HALF_UP
    )

转换文件大小带单位(Convert file size with unit):

fun formatSizeByTypeWithUnit(size: Long, scale: Int, sizeType: FileSizeType): String {
    return "${formatSizeByTypeWithoutUnit(size.toBigDecimal(), scale, sizeType).toPlainString()}${sizeType.unit}"
}

3. 直接打开Url/Uri(远程or本地)👉FileOpener.kt

打开系统分享弹窗(Open the system sharing popup)
fun openShare(context: Context, uri: Uri, title: String = "分享文件") {
    val intent = Intent(Intent.ACTION_SEND)
    intent.putExtra(Intent.EXTRA_STREAM, uri)
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
    // Put the Uri and MIME type in the result Intent
    intent.setDataAndType(uri, getMimeType(uri))
    context.startActivity(Intent.createChooser(intent, title))
}
打开浏览器(Open browser)
@SuppressLint("QueryPermissionsNeeded")
fun openBrowser(
    context: Context, url: String, title: String = "请选择浏览器", newTask: Boolean = false,
    block: ((result: Boolean, msg: String?) -> Unit)? = null,
) {
    try {
        val intent = Intent(Intent.ACTION_VIEW)
        intent.data = Uri.parse(url)
        if (newTask) {
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        }
        //startActivity(intent)
        //https://developer.android.com/about/versions/11/privacy/package-visibility
        if (intent.resolveActivity(context.packageManager) != null) {
            context.startActivity(Intent.createChooser(intent, title))
            block?.invoke(true, null)
        } else {
            block?.invoke(true, "没有可用浏览器")
        }
    } catch (e: ActivityNotFoundException) {
        e.printStackTrace()
        block?.invoke(true, e.toString())
    }
}
直接打开Url对应的系统应用

Directly open the system application corresponding to 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)
    }
}
根据文件路径和类型(后缀判断)显示支持该格式的程序

According to file path and type (judgment by suffix) show programs that support the format

fun openFile(context: Any, uri: Uri?, mimeType: String? = null) =
    uri?.let { u ->
        Intent.createChooser(createOpenFileIntent(u, mimeType), "选择程序")?.let {
            startActivity(context, it)
        }
    }
选择文件【使用系统的文件管理】

Select file [Use system file management]

/**
 * ### 选择文件【调用系统的文件管理】 (Select file [call system file management])
 *
 * 注:
 *
 * #### 1. Intent.setType 不能为空(Can not be empty) !
 * ```
 * android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.OPEN_DOCUMENT cat=[android.intent.category.OPENABLE] (has extras) }
 * at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:2105)
 * ```
 *
 * #### 2. mimeTypes 会覆盖 mimeType (mimeTypes will override mimeType)
 * ```
 * eg:
 *      Intent.setType("image / *")
 *      Intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("audio / *"))
 * 🍎 最终可选文件类型变为音频
 * ```
 *
 * #### 3. ACTION_GET_CONTENT, ACTION_OPEN_DOCUMENT 效果相同, Android Q 上使用 `ACTION_GET_CONTENT` 会出现:
 * ```
 *      java.lang.SecurityException: UID 10483 does not have permission to content://com.android.providers.media.documents/document/image%3A16012 [user 0];
 *      you could obtain access using ACTION_OPEN_DOCUMENT or related APIs
 * ```
 *
 * #### 4. 开启多选(Open multiple selection) resultCode = -1
 *
 * #### 5. 无论是`ACTION_OPEN_DOCUMENT`还是`ACTION_GET_CONTENT`都只是负责打开和选择,
 * 具体的文件操作如查看文件内容,删除,分享,复制,重命名等操作需要在`onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)`中的`data:Intent`中提取
 *
 */
fun createChooseIntent(@NonNull mimeType: String?, @Nullable mimeTypes: Array<String>?, multiSelect: Boolean): Intent =
    Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiSelect)
        type = if (mimeType.isNullOrBlank()) "*/*" else mimeType
        if (!mimeTypes.isNullOrEmpty()) {
            putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
        }
        addCategory(Intent.CATEGORY_OPENABLE)
    }

4. 获取文件Uri/Path👉FileUri.kt

从File路径中获取Uri

Obtain Uri from File path

fun getUriByPath(path: String?): Uri? = if (path.isNullOrBlank()) null else getUriByFile(File(path))

fun getUriByFile(file: File?): Uri? =
    file?.let {
        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

Get the file path corresponding to Uri, compatible with 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)) return uri.path
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) getPath(context, uri) else getPathKitkat(context, uri)
}

5. 通用文件工具类👉FileUtils.kt

MethodRemark
getExtension获取文件后缀jpg
getExtensionFull获取文件完整后缀.jpg
splitFilePath()拆分文件路径 eg: /xxx/xxx/note.txt 👉 path: /xxx/xxx(注:尾部没有/) name: note suffix: txt
getFileNameFromPath(path: String?)通过FilePath获取文件名
getFileNameFromUri(uri: Uri?)通过Uri获取文件名
createFile(filePath: String?, fileName: String?, overwrite: Boolean = false):File?创建文件, 同名文件创建多次会跳过已有创建新的文件,如:note.txt已存在,则再次创建会生成note(1).txt
createDirectory(filePath: String?): Boolean创建目录
deleteFile删除文件或目录
deleteFileWithoutExcludeNames(file: File?, vararg excludeDirs: String?)删除文件或目录, excludeDirs 指定名称的一些文件/文件夹不做删除
deleteFilesNotDir只删除文件,不删除文件夹
readFileText(InputStream/Uri): String?读取文本文件中的内容
readFileBytes(InputStream/Uri): ByteArray?读取文件中的内容并返回ByteArray
copyFile根据文件路径拷贝文件 java.nio
write2File(bitmap:Bitmap, file:File?, overwrite:Boolean=false)把Bitmap写到文件中,可通过BitmapFactory.decodeStream()读取出来
write2File(input:InputStream?, file:File?, overwrite:Boolean=false)向文件中写入数据
isLocal检验是否为本地URI
isGif()检验是否为 gif

copyFile效率和kotlin-stdlib-1.4.21.jar中的kotlin.io.FilesKt__UtilsKt.copyTo基本相当 :

fun File.copyTo(target: File, overwrite: Boolean = false,bufferSize: Int = DEFAULT_BUFFER_SIZE): File

Usage:

boolean copyResult = FileUtils.copyFile(fileOld, getExternalFilesDir(null).getPath(), "test.txt");
File targetFile = new File(getExternalFilesDir(null).getPath() + "/" + "test.txt");

总结(Summary)

  1. onActivityResult 中要把选择文件的结果交给FileSelector处理 :
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    //选择结果交给 FileSelector 处理, 可通过`requestCode -> REQUEST_CHOOSE_FILE`进行区分
    mFileSelector?.obtainResult(requestCode, resultCode, data)
}
  1. 选择文件不满足预设条件时,有两种策略 :

    • OVER_LIMIT_EXCEPT_ALL 文件超过数量或大小限制直接返回失败, 回调 onError

    • OVER_LIMIT_EXCEPT_OVERFLOW ① 文件超过数量限制或大小限制;
      ② 单一类型: 保留未超限制的文件并返回, 去掉后面溢出的部分; 多种类型: 保留正确的文件, 去掉错误类型的所有文件;
      ③ 回调 onSuccess

  2. 选择文件数据:单选 Intent.getData ; 多选 Intent.getClipData

  3. Android 系统问题 : Intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
    开启多选条件下只选择一个文件时,系统是按照单选逻辑走的… Σ( ° △ °|||)︴

  4. Activity中执行getExternalFilesDirs(Environment.DIRECTORY_XXX)和其它获取目录地址的方法时,都会自动创建相应的目录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QioPeGxM-1615774263647)(https://raw.githubusercontent.com/javakam/FileOperator/master/screenshot/img1.png)]

  1. Uri.fromFile(file)生成的file:///...是不能分享的,所以需要使用FileProvider将App Specific目录下的文件分享给其他APP读写,
    需要通过FileProvider解析出的可用于分享的路径: ando.file.core.FileUri.getUriByFile(file)


更新日志 (Update log)

README_VERSIONS.md https://github.com/javakam/FileOperator/blob/master/README_VERSIONS.md

Fixed Bug

1.Invalid image: ExifInterface got an unsupported image format
W/ExifInterface: Invalid image: ExifInterface got an unsupported image format
    file(ExifInterface supports JPEG and some RAW image formats only) or a corrupted JPEG file to ExifInterface.
     java.io.IOException: Invalid byte order: 0
         at android.media.ExifInterface.readByteOrder(ExifInterface.java:3134)
         at android.media.ExifInterface.isOrfFormat(ExifInterface.java:2449)
         at android.media.ExifInterface.getMimeType(ExifInterface.java:2327)
         at android.media.ExifInterface.loadAttributes(ExifInterface.java:1755)
         at android.media.ExifInterface.<init>(ExifInterface.java:1449)
      ...

Fixed :
    dependencies {
        compileOnly "androidx.exifinterface:exifinterface:1.3.2"
        ...
    }

    Then replace `android.media.ExifInterface` with `androidx.exifinterface.media.ExifInterface`
2.ImageDecoder$DecodeException: Failed to create image decoder with message
Caused by: android.graphics.ImageDecoder$DecodeException:
Failed to create image decoder with message 'unimplemented'Input contained an error.

What is new in Android P — ImageDecoder & AnimatedImageDrawable

3.SecurityException… you could obtain access using ACTION_OPEN_DOCUMENT or related APIs
java.lang.SecurityException: UID 10483 does not have permission to
    content://com.android.providers.media.documents/document/image%3A16012 [user 0];
    you could obtain access using ACTION_OPEN_DOCUMENT or related APIs

Fixed: ando.file.core.FileOpener.createChooseIntent
把 Intent(Intent.ACTION_GET_CONTENT) 改为 Intent(Intent.ACTION_OPEN_DOCUMENT)

4.IllegalArgumentException: column ‘_data’ does not exist

https://stackoverflow.com/questions/42508383/illegalargumentexception-column-data-does-not-exist

5.ActivityNotFoundException: No Activity found to handle Intent
android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.OPEN_DOCUMENT cat=[android.intent.category.OPENABLE] (has extras) }
at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:2105)

Fixed: ando.file.core.FileOpener.createChooseIntent:

Intent.setType("image / *")
Intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("audio / *"))
6.android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/com.ando.file.sample/cache exposed beyond app through Intent.getData()

Fixed: AndroidManifest.xml没配置FileProvider

7.Calling startActivity() from outside of an Activity

https://stackoverflow.com/questions/3918517/calling-startactivity-from-outside-of-an-activity-context

Fixed: Intent.createChooser要添加两次FLAG_ACTIVITY_NEW_TASK:

val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

val chooserIntent: Intent = Intent.createChooser(intent, title)
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(chooserIntent)

感谢(Thanks)

Google

Storage Samples Repository

SAF 使用存储访问框架打开文件

SAF API UseCase

管理分区外部存储访问

管理分区外部存储访问 - 如何从原生代码访问媒体文件 & MediaStore增删该查API

处理外部存储中的媒体文件

Android 11 中的隐私权

Android 10 中的隐私权

Blog

LOGO

FileUtils

github/scoped_storage_sample

掘金-Android Q 适配指南 让你少走一堆弯路

Android Q 沙箱适配多媒体文件总结

oppo AndroidQ适配指导

huawei Google Q版本应用兼容性整改指导

许可(LICENSE)

Copyright 2019 javakam, FileOperator Open Source Project

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Android相机拍照方向旋转的解决方案:ExifInterface 一、 碰到的问题 写这篇文章的动机源自于这波迭代中碰到的一个问题: 在IM拍照时,在三星s7 eadge上拍完照片后从sd上拿到的地址设置给Imageview后显示时,图片旋转了90度。But我拍照的时候明明是竖着拍的,相册预览也是竖着的,为什么拿到图片后就成了横着的? 对比了另一台手机锤子坚果U1,没这个问题,因此怀疑是跟相机的机型相关。 想到的解决... 阅读详情

相关推荐

Android多功能文件选择器,支持访问Android/data(obb)等系统文件夹

Android文件选择器,高度可定制化,支持访问系统data/obb文件夹 顶部导航栏可点击 长按条目可选择 可自定义拓展功能 gradle:project 中 gradle:app 中 Manifest中 依赖与权限设置完成后 在你的Activity中 获取返回的数据 自定义文件显示和图标 自定义图标及字体 类与方法 FileSelectorTheme 下面图片中给出了UI的属性作用位置的序号:表中每个属性均有set方法,并有默认的属性,故该设置不是必须的设置。接口抽象方法:onclick

weixin_44370506的博客 8562

图片工具类

这是项目中用到的图片工具类,记录于此。 import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.media.ExifInterface; import java.io.File; import java.io.Fil...

luoyong_blog的博客 386

Android开发之使用ExifInterface获取拍照后的图片属性

本文实例讲述了Android开发之使用ExifInterface获取拍照后的图片属性。分享给大家供大家参考,具体如下: ExifInterface exif = new ExifInterface(file.getPath()); String widthStr = exif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH); String heightStr = exif.getAttribute(ExifInterface.TAG_IMAGE_LENGTH); ...... /* * Copyright (C) 2007 The Android

Android之exif格式 ExifInterface

## 什么是exif?&amp;gt;Exif是一种图像文件格式,它的数据存储与[JPEG](https://baike.baidu.com/item/JPEG)格式是完全相同的。实际上Exif格式就是在JPEG格式头部插入了数码照片的信息,包括拍摄时的[光圈](https://baike.baidu.com/item/%E5%85%89%E5%9C%88)、[快门](https://baike.baid...

weixin_42128489的博客 1149

AI大模型与碳足迹评估结合模式及示范案例

AI大模型与碳足迹评估相结合,可以提供更精确、更快速的碳排放量计算,优化减排策略,并促进可持续发展目标的实现。

贾贾的博客 3943

科研作图不发愁:手把手教你用PyMOL给蛋白-DNA互作图‘美颜’(附完整配色与标注方案)

本文详细介绍了如何使用PyMOL对蛋白-DNA互作结构图进行专业美化,包括从PDB文件加载、结构展示与标注、互作位点突出显示到高质量渲染输出的完整流程。通过具体案例(PDB ID:8SSS)演示了锌指结构域区分、关键残基标注和互作距离测量等实用技巧,帮助科研人员制作可直接用于期刊投稿的高质量示意图。

weixin_42523792的博客 552

推荐项目:FileOperator - 安卓端高效文件管理利器

在安卓开发领域,高效、稳定的文件管理解决方案一直是开发者追求的目标之一。今天,我们来探索一款专为Android平台设计的文件操作库——**FileOperator**。它不仅完美适配了从Android 4.4至最新系统的环境,还特别针对Android Q及Android 11的新存储策略进行了全面优化,大大简化了开发过程中的文件管理难题。 ## 项目技术分析 FileOperator提供了一套...

gitblog_01190的博客 565

文本→图像→视频:AIGC跨模态转换全流程解析

本文旨在系统性地解析AIGC领域中跨模态内容生成的技术原理和实现方法,特别聚焦于从文本到图像再到视频的完整转换流程。跨模态表示学习的基本原理文本到图像生成的核心技术图像到视频转换的关键算法端到端跨模态生成系统的构建方法实际应用场景和性能优化策略背景介绍:建立基本概念和知识框架核心概念:解析跨模态转换的关键技术和原理算法细节:深入讲解文本到图像、图像到视频的核心算法数学模型:提供必要的数学基础和公式推导项目实战:通过完整案例展示实现过程应用场景:探讨技术的实际应用可能性。

AI天才研究院 1353

突破Android存储壁垒:FileOperator全方位文件操作解决方案

项目代码托管在GitCode: ``` https://gitcode.com/gh_mirrors/fi/FileOperator ``` 欢迎通过以下方式贡献代码: 1. Fork项目并提交Pull Request 2. 报告Bug或提出功能建议 3. 改进文档和示例代码 4. 分享使用经验和最佳实践...

gitblog_00645的博客 852

别再手动配IP了!用NI-USRP Configuration Utility快速搞定USRP 2954与LabVIEW连接

本文详细介绍了如何使用NI-USRP Configuration Utility快速配置USRP 2954与LabVIEW的连接,解决传统手动IP配置的繁琐问题。通过图形化工具自动完成设备发现、IP设置和连接验证,大幅提升软件无线电实验效率,特别适合信号收发应用场景。

weixin_27102543的博客 292

【城市数据集】世界城市数据库和访问门户工具WUDAPT

简介世界城市数据库和访问门户工具WUDAPT

WW、forever的博客 1286

解决ImageIo图像类型不支持的异常:Unsupported Image Type

在做图片压缩时,遇到如下错误: javax.imageio.IIOException: Unsupported Image Type 原因是jdk自带的jpeg解析器不能解析所有jpeg格式的图片,如cmyk(印刷品模式)模式的图片。ge Type 图片经过PS处理之后,保存时默认的保存格式是cmyk格式的,而不是rgb格式。 RGB色彩和CMYK色彩的区别: RGB色彩: 自然界中绝大部分的可见光谱可以用红、绿和蓝三色光按不同比例和强度的混合来表示。RGB分别代表着3种颜色:R代表红色,G代

m0_57556230的博客 4704

OpenClaw第2章:快速安装

本章将手把手教你安装 OpenClaw。

Maynor的博客 3236

Luban

Luban前言一、Luban是什么?二、使用步骤1.引入库2.使用3.API三、算法作者地址 前言 自己写的bitmap压缩不够优秀,找了个优秀的图片压缩框架 一、Luban是什么? Luban(鲁班) —— Android图片压缩工具,仿微信朋友圈压缩策略。 二、使用步骤 1.引入库 implementation 'top.zibin:Luban:1.1.8' 2.使用 一张一张压缩 Observable.fromIterable(images ==

嗯... 2512

PCL 区域生长分割【2026最新版】

区域生长算法原理介绍、代码实现以及一个具体实验场景的应用案例进一步实现将分割结果的分类保存至本地文件夹。最近一次更新时间为:2026年5月4日。

点云侠的博客 40万+

在 SAP GUI 里开发与调用 Adobe PDF 表单:SFP 建模 + Driver Program 调用的全流程实战

本文介绍了在SAP系统中使用Adobe PDF表单的完整流程。重点阐述了SAP Interactive Forms相比传统表单工具的优势,特别是在处理复杂排版、条码和交互式字段时的适用性。文章详细说明了开发流程:从安装设计工具Adobe LiveCycle Designer开始,到在SFP事务码中创建Interface和Form的数据模型,再到编写驱动程序的ABAP代码实现PDF输出。特别强调了Interface作为数据契约的重要性,以及如何通过分层设计实现前后端分离开发。最后提供了标准调用链的示例代码,展

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 276

【Android】 Exif 接口

我们在应用程序中广泛使用图像。对于某些图像相关应用程序,需要图像的所有详细信息。但是我们不能总是直接从图像中读取图像的细节。可能需要了解图像详细信息,例如 GPS 位置、日期/时间、拍摄时的设置、方向等。以前我们很难获取图像信息,为此我们必须从不同的方法中分别提取元数据并读取它以了解图像属性。即使当我们阅读时,对图像的更改(例如删除 GPS 标签或更改方向)也将是一项具有挑战性的任务。这些的解决方案是 Exif 文件和 ExifInterface。让我们了解一下EXIF是什么?Exif 代表可交换图像文件格

m0_56144365的博客 2190

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

Android开发问题,未整理

机型:荣耀10 1:图片裁切报错:java.lang.SecurityException: UID 11068 does not have permission to content://com.android.providers.media.documents/document/image%3A1930929 [user 0]; you could obtain access using ACTION_OPEN_DOCUMENT or related APIs 启动裁切页面部分注释下去掉以下语句 inte

Drumsticks的博客 788

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

探索高效文件操作利器:FileOperator

在日常的软件开发或数据分析工作中,我们经常需要对大量的文件进行处理和管理。 是一个强大的Java库,它提供了一系列便捷的API,让你能够更加高效、灵活地完成文件及目录的相关操作。无论你是新手还是经验丰富的开发者,这款开源工具都值得你深入了解并加入你的工具箱。 ## 项目简介 FileOperator 是由 JavaKam 开发的一款面向开发者的设计精良的文件操作库。它的核心目标是简化文件操作的...

gitblog_00032的博客 676

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

FileOperator 文件管理框架使用指南

**FileOperator** 是一个专为Android开发者设计的文件管理框架,它提供了一系列简洁、易用的工具类,使得在Android系统中进行文件操作变得更加便捷。该框架支持从Android 4.4(KitKat)到最新的Android版本,包括对Android Q及以上版本新存储策略的兼容。FileOperator还包含了图片压缩算法,以及其他与MediaStore和SAF相关的实用操作。...

gitblog_00830的博客 715

that you obtain access using ACTION_OPEN_DOCUMENT or related APIs

Permission Denial: reading com.android.providers.media.MediaDocumentsProvider uri content://com.android.providers.media.documents/document/document%3A4287 from pid=27943, uid=10152 requires that you o...

情随事迁个人博客 379

拍照/图库选择——裁剪 问题记录

拍照/图库选择——裁剪 遇到的权限问题解决

海蓝的博客 475

FileOperator 项目常见问题解决方案

**项目介绍:** FileOperator 是一个适用于Android系统的文件操作库。它提供了包括文件的创建、删除、复制、打开,以及获取文件大小、名称、后缀、MimeType等功能。此外,它还支持MediaStore和Storage Access Framework (SAF)的相关操作,并且解决了获取文件Uri/Path的兼容性问题,支持图片压缩和文件选择等功能。该库适用于Android 4...

gitblog_00431的博客 891

Android crash解决集锦

问题描述: 外接U盘长点击一文件夹选中后拨出OTG线提示停止运行 log内容: ----- timezone:GMT 11-22 10:19:48.753412 5655 5655 E AndroidRuntime: FATAL EXCEPTION: main 11-22 10:19:48.753412 5655 5655 E AndroidRuntime: Process: com

颂一家之言 3034

获取PPT中的MSO格式图片报错

在这个代码中,我们首先尝试使用 PIL 打开图片。如果图片成功打开,我们检查其格式是否在支持的格式列表中。如果不在,我们将图片转换为 RGB 格式,并将其保存为 JPEG。如果图片格式是支持的,我们直接使用原始格式保存图片。错误信息表明,您正在使用一个期望得到特定图像格式列表的方法或函数,但是传递给它的格式是 ‘MPO’,这不在预期的格式列表中。

qq_48131648的博客 292

Android 11 从外部存储读取文件到应用沙盒存储

Android10之前,访问外部存储目录只需要Environment.getExternalStorageDirectory().getPath()获取根目录,再通过new File()的形式访问。 Android 10 开始,Google建议开发者使用文件选择访问外部存储,但是没有强制。 Android 11以上,开始强制不能再通过new File()的形式访问外部存储了。 效果: 1. FileHandlePresenter.java实现。 封装方法:发Intent拉起文件选择器 ..

xiaobaaidaba123的专栏 8771
上一篇: Android Fragment懒加载
下一篇: Android Q & Android 11存储适配(一) 基础知识点梳理
风之旅人、
博客等级 码龄11年 5粉丝 15原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值