IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> android 媒体文件分区适配 -> 正文阅读

[移动开发]android 媒体文件分区适配

android 媒体文件分区适配

最近刚好有个需求做了媒体文件的分区适配,记录以下
官方文档地址:https://developer.android.com/guide/topics/data?hl=zh-cn

问题点

之前的下载文件都在/storage/emulated/0/android/data/pagename/file中,首先这个属于应用内部的文件系统,而且是不需要读写权限的申请的,它会跟随应用的卸载而删除

由于Android Q文件存储机制修改成了沙盒模式,同时对应用内部的文件系统加强了访问控制,就是一般用户都看不到这个文件,导致下载了,用户也找不到,即使用了getContentResolver().insert能看到但是不能播放.

主要目的:

  • 非必要少去申请权限
  • 不跟随应用卸载而删除
  • 下载后方便查看和播放
  • 非隐私文件

解决

Android Q文件存储机制修改成了沙盒模式,对于图片和视频这种媒体文件google希望是放在手机自带的媒体文件夹中。
参考资料:https://developer.android.com/training/data-storage/shared/media?hl=zh-cn

手机自带的媒体文件:

  • 图片,存储在 DCIM/ 和 Pictures/ 目录中。系统将这些文件添加到 MediaStore.Images 表格中。
  • 视频,存储在 DCIM/、Movies/ 和 Pictures/ 目录中。系统将这些文件添加到 MediaStore.Video 表格中。
  • 音频文件,存储在 Alarms/、Audiobooks/、Music/、Notifications/、Podcasts/ 和 Ringtones/ 目录中,以及位于 Music/ 或 Movies/ 目录中的音频播放列表中。系统将这些文件添加到 MediaStore.Audio 表格中。
  • 下载的文件,存储在 Download/ 目录中。在搭载 Android 10(API 级别 29)及更高版本的设备上,这些文件存储在 MediaStore.Downloads 表格中。此表格在 Android 9(API 级别 28)及更低版本中不可用。

具体的解决方案:将文件存储在媒体库里。
适配方案:google在android10上,已经开启的存储分区,但是也可以关闭android:requestLegacyExternalStorage="true" ,但是在android11上是强制开启的,所以主要的适配方案为:android11以下 和 android11及以上。

代码

下载

android11及以上

@RequiresApi(Build.VERSION_CODES.Q)
    fun downR(fileName: String, relativePath: String, inputStream: InputStream) {
        val resolver = contentResolver
        val audioCollection = MediaStore.Video.Media
            .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)

        val videoDetails = ContentValues().apply {
            //设置文件名字
            put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
            //设置文件类型
            put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
            //先开启IS_PENDING,如果文件很大,执行耗时,将 IS_PENDING 标记的值设为 1 来获取此独占访问权限
            put(MediaStore.Video.Media.IS_PENDING, 1)
            //设置所属包
            put(MediaStore.Video.Media.OWNER_PACKAGE_NAME, packageName)
            //RELATIVE_PATH 表示该文件在媒体文件下的相对路径, 方便管理和查询
            put(MediaStore.Video.Media.RELATIVE_PATH, relativePath)
        }
        val videoContentUri = resolver.insert(audioCollection, videoDetails)

        videoContentUri?.let {
            resolver.openFileDescriptor(videoContentUri, "w", null).use { pfd ->
                //记住切IO线程,这里切 外面切都行

                //打开文件写入流
                val output = FileOutputStream(pfd?.fileDescriptor)
                //开始写入文件
                output.use {out->
                    inputStream.copyTo(out) {
                        println("进度:$it")
                    }
                }
                videoDetails.clear()
                //清楚独占访问权限
                videoDetails.put(MediaStore.Audio.Media.IS_PENDING, 0)
                //更新到媒体文件夹中
                resolver.update(videoContentUri, videoDetails, null, null)
            }
        }
    }

inline fun InputStream.copyTo(
    out: OutputStream,
    bufferSize: Int = DEFAULT_BUFFER_SIZE,
    process: (Long) -> Unit
): Long {
    var bytesCopied: Long = 0
    val byteArray = ByteArray(bufferSize)
    var bytes = read(byteArray)
    while (bytes > 0) {
        out.write(byteArray, 0, bytes)
        bytesCopied += bytes
        bytes = read(byteArray)
        process.invoke(bytesCopied)
    }
    return bytesCopied
}

android11以下

就是普通的文件写入操作,唯一的区别就是路径要跟android11上面下载的路径一致。

fun down(fileName: String, relativePath: String, inputStream: InputStream) {
        val savePath = getMoviesFileCache(fileName)
        //打开文件写入流
        val output: OutputStream = FileOutputStream(savePath)
        //开始写入文件
        output.use {out->
            inputStream.copyTo(out) {
                println("进度:$it")
            }
        }
    }
 fun getMoviesFileCache(fileName: String): File? {
        //创建项目图片公共缓存目录
        val file = File(
            Environment.getExternalStorageDirectory().absolutePath + File.separator +
                    Environment.DIRECTORY_MOVIES + "你的相对路径,android11上的RELATIVE_PATH"
        )
        if (!file.exists()) {
            file.mkdirs()
        }
        //创建对应图片的缓存路径
        return File(file.absolutePath + File.separator + fileName);
    }

查询

android11

 private fun queryMediaBean(fileName: String): MediaStoreBean? {
        val projection = arrayOf(
            MediaStore.Video.Media._ID,
            MediaStore.Video.Media.DISPLAY_NAME,
            MediaStore.Video.Media.DATE_ADDED,
            MediaStore.Video.Media.RELATIVE_PATH,
        )
        //查询语句 ?为参数的占位符 
        //多条查询 用 and 连接
        // 查询 RELATIVE_PATH 下 的 名字为 DISPLAY_NAME
        val selection1 =
            "${MediaStore.Video.Media.RELATIVE_PATH} = ? and ${MediaStore.Video.Media.DISPLAY_NAME} = ?"
            //占位符的 参数
        val selectionArgs1 = arrayOf(
            "你的相对路径",
            fileName
        )
        val sortOrder = "${MediaStore.Video.Media.DATE_ADDED} DESC"
        BaseApplication.context.contentResolver.query(
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
            projection,
            selection1,
            selectionArgs1,
            sortOrder
        )?.use { cursor ->
            val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
            val dateModifiedColumn =
                cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_ADDED)
            val displayNameColumn =
                cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
            val relativePathColumn =
                cursor.getColumnIndexOrThrow(MediaStore.Video.Media.RELATIVE_PATH)

            while (cursor.moveToNext()) {

                // Here we'll use the column indexs that we found above.
                val id = cursor.getLong(idColumn)
                val dateModified =
                    Date(TimeUnit.SECONDS.toMillis(cursor.getLong(dateModifiedColumn)))
                val displayName = cursor.getString(displayNameColumn)
                val contentUri = ContentUris.withAppendedId(
                    MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
                    id
                )
                val relativePath = cursor.getString(relativePathColumn)
            }
        }
       
    }

android11以下

检测文件是否存在就行了

fun queryLocalVideoExist(vehicleId: String, fileName: String) : Boolean {
        val filePath: String = us.nonda.zus.download.util.FileUtil.getDCVideoPath(vehicleId+"_"+fileName)
        val file = File(filePath)
        return file.exists()
    }

删除

android11

android11删除文件很麻烦,需要2次确认

 private fun performDeleteImage(image: MediaStoreImage) {
 //MediaStoreImage 就是一个普通的bean,保存了对应媒体文件的id、contentUri、displayName等属性。
        try {
            this.contentResolver.delete(
                image.contentUri,
                "${MediaStore.Images.Media._ID} = ?",
                arrayOf(image.id.toString())
            )
        } catch (securityException: SecurityException) {
            if (VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                val b = securityException is RecoverableSecurityException
                val recoverableSecurityException =
                    securityException as? RecoverableSecurityException
                        ?: throw securityException

                // Signal to the Activity that it needs to request permission and
                // try the delete again if it succeeds.
                pendingDeleteImage = image
                startIntentSender(recoverableSecurityException.userAction.actionIntent.intentSender)
            } else {
                throw securityException
            }
        }
    }
	private val DELETE_PERMISSION_REQUEST = 0x1033
    private fun startIntentSender(intentSender: IntentSender?) {
        intentSender?.let {
            startIntentSenderForResult(
                intentSender,
                DELETE_PERMISSION_REQUEST,
                null,
                0,
                0,
                0,
                null
            )
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK && requestCode == DELETE_PERMISSION_REQUEST) {
            pendingDeleteImage?.let {
                pendingDeleteImage = null
                performDeleteImage(it)
            }
        }
    }

android11以下

  public static void deleteDCVideo(String fileName) {
        try {
            String filePath = FileUtil.getDCVideoPath(fileName);
            File file = new File(filePath);
            if (file.exists()) {
                file.delete();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-01-28 12:02:03  更:2022-01-28 12:03:02 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 13:03:07-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码