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

上一篇 👉 Android Q & Android 11存儲適配(一) 基礎知識點梳理php

FileOperator

  • 🚀更簡單的處理Android系統文件操做
  • 🚀適用於Android 4.4及以上系統, 兼容AndroidQ新的存儲策略
  • 🚀圖片壓縮算法參考 👉 Luban
  • 🚀Kotlin Sample 👉 app
  • 🚀Java Sample 👉 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.)html

預覽(Preview)

功能列表(Function list) 緩存目錄(Cache directory)

文件選擇(File selection)

單圖+壓縮(Single Image+Compress) 多圖+壓縮(Multiple images+Compress) 多文件+多類型(Multiple files+Multiple types)

用法(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)java

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)android

🌴適用於處理複雜文件選擇情形, 如: 選取圖片、音頻文件、文本文件, 其中圖片至少選擇一張, 最多選擇兩張, 每張圖片大小不超過5M, 所有圖片大小不超過10M;git

音頻文件至少選擇兩個, 最多選擇三個, 每一個音頻大小不超過20M, 所有音頻大小不超過30M; 文本文件至少選擇一個, 最多選擇兩個, 每一個文本文件大小不超過5M, 所有文本文件大小不超過10Mgithub

🌴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 File Choose 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 10Mweb

//圖片 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 typeexpress

🍎下面提供了兩種實現的方式 (The following provides two ways):apache

//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/*打開, 對應的mimeTypeapplication/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

Method Remark
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)

開啓多選條件下只選擇一個文件時,系統是按照單選邏輯走的... Σ( ° △ °|||)︴

  1. Activity中執行getExternalFilesDirs(Environment.DIRECTORY_XXX)和其它獲取目錄地址的方法時,都會自動建立相應的目錄

  1. Uri.fromFile(file)生成的file:///...是不能分享的,因此須要使用FileProviderApp Specific目錄下的文件分享給其餘APP讀寫,

須要經過FileProvider解析出的可用於分享的路徑: ando.file.core.FileUri.getUriByFile(file)


更新日誌 (Update log)

README_VERSIONS.md github.com/javakam/Fil…

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

stackoverflow.com/questions/4…

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

stackoverflow.com/questions/3…

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.
複製代碼
相關文章
相關標籤/搜索