Android通过okhttp下载文件(本文案例 下载mp4到本地,并更新到相册)

使用步骤分为两步

第一步导入 okhttp3 依赖

第二步调用本文提供的 utils

第一步这里不做说明了,直接提供第二步复制即用
DownloadUtil 中 download 为下载文件 参数说明

这里主要看你把 destFileName 下载文件名称定义为什么后缀,比如我定义为 .mp4 下载后 就是 mp4 格式

这里 destFileDir 下载目录要说一下,如果没有开启存储权限或者使用了系统默认路径就会报错 比如 /0 文件一类的错误,怎么使用可以参考 open failed: ENOENT (No such file or directory) 解决办法

DownloadUtil 中 saveVideoToAlbum 为将下载好的视频更新到手机图库中,原来的放式已经随着安全性提高不适用了,这里基本就是复制出一份更新到系统层的文件夹

源码

java 复制代码
DownloadUtil.download(mVideoUrl,getUrlPath(),"sing示例名称${System.currentTimeMillis()}.mp4",object : DownloadUtil.OnDownloadListener{
                override fun onDownloadSuccess(file: File?) {
                    "下载成功".toast()
                    //更新视频到相册
                    DownloadUtil.saveVideoToAlbum(this@MoreActivity,file?.absolutePath)
                    Log.e("视频下载", "下载成功: ${file?.absolutePath}")
                }

                override fun onDownloading(progress: Int) {
                    Log.e("视频下载", "下载进度:${progress}")
                }

                override fun onDownloadFailed(e: Exception?) {
                    LoadingSingDialog.dismiss()
                    Log.e("视频下载", "下载失败:${e?.printStackTrace()}")
                }

            })

DownloadUtil

java 复制代码
object DownloadUtil {

    private var okHttpClient: OkHttpClient? = null

    /**
     * @param url          下载连接
     * @param destFileDir  下载的文件储存目录
     * @param destFileName 下载文件名称
     * @param listener     下载监听
     */
    fun download(url: String, destFileDir: String, destFileName: String, listener: OnDownloadListener) {
        if (url == null || url == ""){
            return
        }
        if (okHttpClient == null){
            okHttpClient = OkHttpClient()
        }
        val request: Request = Request.Builder().url(url).build()
        okHttpClient!!.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                // 下载失败监听回调
                listener.onDownloadFailed(e)
            }

            @Throws(IOException::class)
            override fun onResponse(call: Call, response: Response) {
                if (response.body != null) {
                    var inputStream: InputStream? = null
                    val buf = ByteArray(2048)
                    var len = 0
                    var fos: FileOutputStream? = null
                    // 储存下载文件的目录
                    val dir = File(destFileDir)
                    if (!dir.exists()) {
                        dir.mkdirs()
                    }
                    val file = File(dir, destFileName)
                    try {
                        inputStream = response.body!!.byteStream()
                        val total: Long = response.body!!.contentLength()
                        fos = FileOutputStream(file)
                        var sum: Long = 0
                        while (inputStream.read(buf).also { len = it } != -1) {
                            fos.write(buf, 0, len)
                            sum += len.toLong()
                            val progress = (sum * 1.0f / total * 100).toInt()
                            // 下载中更新进度条
                            listener.onDownloading(progress)
                        }
                        fos.flush()
                        // 下载完成
                        listener.onDownloadSuccess(file)
                    } catch (e: Exception) {
                        listener.onDownloadFailed(e)
                    } finally {
                        try {
                            inputStream?.close()
                        } catch (e: IOException) {
                            listener.onDownloadFailed(e)
                        }
                        try {
                            fos?.close()
                        } catch (e: IOException) {
                            listener.onDownloadFailed(e)
                        }
                    }
                }else{
                    listener.onDownloadFailed(IOException("接口失败"))
                }
            }
        })
    }

    interface OnDownloadListener {
        /**
         * @param file 下载成功后的文件
         */
        fun onDownloadSuccess(file: File?)

        /**
         * @param progress 下载进度
         */
        fun onDownloading(progress: Int)

        /**
         * @param e 下载异常信息
         */
        fun onDownloadFailed(e: Exception?)
    }


    /**
     * 将视频保存到系统相册
     */
    fun saveVideoToAlbum(context: Context, videoFile: String?): Boolean {
        if (videoFile == null || videoFile == ""){
            return false
        }
        return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
            saveVideoToAlbumBeforeQ(context, videoFile)
        } else {
            saveVideoToAlbumAfterQ(context, videoFile)
        }
    }

    private fun saveVideoToAlbumAfterQ(context: Context, videoFile: String): Boolean {
        return try {
            val contentResolver = context.contentResolver
            val tempFile = File(videoFile)
            val contentValues = getVideoContentValues(context, tempFile, System.currentTimeMillis())
            val uri =
                contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues)
            copyFileAfterQ(context, contentResolver, tempFile, uri)
            contentValues.clear()
            contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
            context.contentResolver.update(uri!!, contentValues, null, null)
            context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri))
            true
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
            false
        }
    }

    private fun saveVideoToAlbumBeforeQ(context: Context, videoFile: String): Boolean {
        val picDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
        val tempFile = File(videoFile)
        val destFile = File(picDir, context.packageName + File.separator + tempFile.name)
        var ins: FileInputStream? = null
        var ous: BufferedOutputStream? = null
        return try {
            ins = FileInputStream(tempFile)
            ous = BufferedOutputStream(FileOutputStream(destFile))
            var nread = 0L
            val buf = ByteArray(1024)
            var n: Int
            while (ins.read(buf).also { n = it } > 0) {
                ous.write(buf, 0, n)
                nread += n.toLong()
            }
            MediaScannerConnection.scanFile(
                context, arrayOf(destFile.absolutePath), arrayOf("video/*")
            ) { path: String?, uri: Uri? -> }
            true
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
            false
        } finally {
            try {
                ins?.close()
                ous?.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }

    @Throws(IOException::class)
    private fun copyFileAfterQ(
        context: Context,
        localContentResolver: ContentResolver,
        tempFile: File,
        localUri: Uri?
    ) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
            context.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.Q
        ) {
            //拷贝文件到相册的uri,android10及以上得这么干,否则不会显示。可以参考ScreenMediaRecorder的save方法
            val os = localContentResolver.openOutputStream(localUri!!)
            Files.copy(tempFile.toPath(), os)
            os!!.close()
            tempFile.delete()
        }
    }


    /**
     * 获取视频的contentValue
     */
    private fun getVideoContentValues(context: Context, paramFile: File, timestamp: Long): ContentValues {
        val localContentValues = ContentValues()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            localContentValues.put(
                MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM
                        + File.separator + context.packageName
            )
        }
        localContentValues.put(MediaStore.Video.Media.TITLE, paramFile.name)
        localContentValues.put(MediaStore.Video.Media.DISPLAY_NAME, paramFile.name)
        localContentValues.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
        localContentValues.put(MediaStore.Video.Media.DATE_TAKEN, timestamp)
        localContentValues.put(MediaStore.Video.Media.DATE_MODIFIED, timestamp)
        localContentValues.put(MediaStore.Video.Media.DATE_ADDED, timestamp)
        localContentValues.put(MediaStore.Video.Media.SIZE, paramFile.length())
        return localContentValues
    }

}
相关推荐
石山岭12 小时前
自己动手写了一个 Android 虚拟定位 App:GPSSimulate 技术实
android·前端
杉氧14 小时前
副作用 (Side Effects) 全攻略:如何像大师一样掌控 Composable 的生命周期?
android·架构·android jetpack
Kapaseker18 小时前
Kotlin Toolchain 0.11 发布:主要是把 Amper 干没了
android·kotlin
三少爷的鞋19 小时前
Android 现代架构不需要事件总线进阶篇
android
杉氧1 天前
深入理解 Compose 重组机制:快照系统如何驱动 UI 精准刷新?
android·架构·android jetpack
召钱熏1 天前
状态枚举正确≠渲染正确:一个语音按钮的状态机边界修复实录
android·前端
杉氧1 天前
深度解析:Jetpack Compose 核心架构与底层原理 —— 十年安卓老兵的“破茧重生”
android·架构·android jetpack
通玄1 天前
Jetpack Compose 入门系列(七):ViewModel 与界面状态管理
android
落魄Android在线炒饭1 天前
Android Framework 开发技巧:android.jar 生成与系统快速编译验证
android
如此风景1 天前
Kotlin Flow操作符学习
android·kotlin