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

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

FileOperator

  • github.com/javakam/Fil…
  • ⭐更簡單的處理Android系統文件操做
  • ⭐適用於 Android 4.4 及以上系統 , 兼容AndroidQ新的存儲策略
  • ⭐圖片壓縮模塊修改自 Luban,兼容更高版本

Gradle:

implementation 'com.ando.file:FileOperator:0.8.0'
複製代碼

Usage:

初始化 Application.onCreated

FileOperator.init(this,BuildConfig.DEBUG)
複製代碼

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 {
            when (fileType) {
                FileType.IMAGE -> {
                    return (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri))
                }
                FileType.VIDEO -> true
                FileType.AUDIO -> true
                else -> true
            }
            return true
        }
    })
    .callback(object : FileSelectCallBack {
        override fun onSuccess(results: List<FileSelectResult>?) {
            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 {
            when (fileType) {
                FileType.IMAGE -> {
                    return (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri))
                }
                FileType.VIDEO -> true
                FileType.AUDIO -> true
                else -> true
            }
            return true
        }
    })
    .callback(object : FileSelectCallBack {
        override fun onSuccess(results: List<FileSelectResult>?) {
            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。android

//圖片
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 {
            when (fileType) {
                FileType.IMAGE -> {
                    return (uri != null && !uri.path.isNullOrBlank() && !FileUtils.isGif(uri))
                }
                FileType.VIDEO -> true
                FileType.AUDIO -> true
                else -> true
            }
            return true
        }
    })
    .callback(object : FileSelectCallBack {
        override fun onSuccess(results: List<FileSelectResult>?) {
            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

//T 爲 String.filePath / Uri / File
fun <T> compressImage(photos: List<T>) {
    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

2. 計算文件或文件夾的大小👉FileSizeUtils.kt

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

4. 獲取文件Uri/Path👉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

  • 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)git

  2. 選擇文件不知足預設條件時,有兩種策略 :github

    • 1.當設置總文件大小限制時,有兩種策略 OVER_SIZE_LIMIT_ALL_DONT 只要有一個文件超出直接返回 onErrorc#

    • 2.OVER_SIZE_LIMIT_EXCEPT_OVERFLOW_PART 去掉超過限制大小的溢出部分的文件segmentfault

  3. 選擇文件數據:單選 Intent.getData ; 多選 Intent.getClipData緩存

  4. Android 系統問題 : Intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) 開啓多選條件下只選擇一個文件時,須要安裝單選邏輯走... Σ( ° △ °|||)︴bash

  5. 回調處理app

多選模式下,建議使用統一的 CallBack 回調;
單選模式下,若是配置了自定義的 CallBack , 則優先使用該回調;不然使用統一的 CallBack框架

  1. Activity中全部能夠調用到的文件夾都是不須要任何權限就能使用 : eg:Activity 👉 getFilesDir or getCacheDir
例如建立壓縮圖片的臨時緩存目錄:
val path = "$filesDir/temp/image/"
複製代碼

將來任務

1.作一個自定義UI的文件管理器
2.增長Fragment使用案例 , 視頻壓縮-郭笑醒 , 清除緩存功能  , 外置存儲適配
3.整理更詳細的文檔 配合 com.liulishuo.okdownload 作文件下載 👉 library_file_downloader
4.
複製代碼

參考

  • Google
  1. Storage Samples Repository

  2. SAF 使用存儲訪問框架打開文件

  3. SAF API UseCase

管理分區外部存儲訪問 管理分區外部存儲訪問 - 如何從原生代碼訪問媒體文件 & MediaStore增刪該查API

處理外部存儲中的媒體文件

Android 11 中的隱私權

Android 10 中的隱私權

  • Other

github/scoped_storage_sample

掘金-Android Q 適配指南 讓你少走一堆彎路

Android Q 沙箱適配多媒體文件總結

oppo AndroidQ適配指導

huawei Google Q版本應用兼容性整改指導

  • 參考項目

MaterialFiles

Shelter

FileUtils

cloud-player-android-sdk

library_file_downloader

項目基於 OkDownload 實現

相關文章
相關標籤/搜索