Android使用FTP协议进行文件下载,获取进度,实现在线升级安装

最近做的一个小需求,通过ftp实现app的在线升级,用的是commons-net-3.3.jar,可支持断点续传,并打印进度。

1,首先网上下载jar包并导入项目;

2,创建连接ftp服务器的类

连接ftp服务器:

fun ftpConnect(): Boolean {
        ftpClient = FTPClient()
        var reply = -1
        try {
            ftpClient!!.apply {
                defaultTimeout = 10000
                connectTimeout = 10000
                setDataTimeout(10000)
                controlEncoding = "UTF-8"
            }
            //连接至服务器
            ftpClient!!.connect(hostname, port)
            //获取响应值
            reply = ftpClient!!.replyCode
            if (!FTPReply.isPositiveCompletion(reply)) {
                ftpClient!!.disconnect()
                throw IOException("connect fail:$reply")
            }
            //登录到服务器
            val login = ftpClient!!.login(username, password)
            //获取响应值
            reply = ftpClient!!.replyCode
            if (!FTPReply.isPositiveCompletion(reply)) {
                ftpClient!!.disconnect()
                throw IOException("connect fail:$reply")
            } else {
                //获取登录信息
                val config = FTPClientConfig(ftpClient!!.systemType.split(" ")[0])
                config.serverLanguageCode = "zh"
                ftpClient!!.configure(config)
                //使用被动模式设为默认
                ftpClient!!.enterLocalPassiveMode()
                //二进制文件支持
                ftpClient!!.setFileType(FTP.BINARY_FILE_TYPE)
                Log.d("FTPUtil", "Success:login FtpClient")
            }
            return login
        } catch (e: Exception) {
            e.printStackTrace()
            Log.e("FTPUtil", "Error: could not connect to host $hostName")
        }
        return false
    }

断开ftp连接:

fun disConnect() {
        if (ftpClient != null) {
            if (ftpClient!!.isConnected) {
                try {
                    ftpClient!!.logout()
                    ftpClient!!.disconnect()
                    Log.d("FTPUtil", "FtpClient logout ")
                } catch (e: Exception) {
                    e.printStackTrace()
                }
            }
        }
    }

列出根目录下所有文件:

 /**
     * 列出FTP下所有文件
     */
    fun listFiles(): MutableList<FTPFile> {
        if (ftpClient != null) {
            try {
                val files = ftpClient!!.listFiles(REMOTE_PATH)
                if (files != null && files.isNotEmpty()) {
                    list.addAll(files.filter { it.name != "." && it.name != ".." && !it.isDirectory })
                }
            } catch (e: Exception) {
                Log.e("FTPUtil", "wait... ")
            }
        }
        return list
    }

我ftp服务器上根目录下就两个文件,一个历史文件夹,一个最新的安装包,这里我直接拿到最新的安装包就行了,后续如果要做版本回退就读取历史文件夹下的文件就行。

文件下载:

/**
     * 单文件下载
     */
    fun download(localFile: File, ftpFile: FTPFile): Boolean {
        if (localFile.exists()) {
            val localBeginSize = localFile.length()
            if (localBeginSize == ftpFile.size) {
                Log.e("FTPUtil", "文件已存在")
            } else if (localBeginSize > ftpFile.size) {
                Log.e("FTPUtil", "文件下载出错")
            }
            return downloadByUnit(ftpFile.name, localFile, localBeginSize, ftpFile.size)
        } else {
            return downloadByUnit(ftpFile.name, localFile, 0, ftpFile.size)
        }


    }
private fun downloadByUnit(
        remote: String,
        localFile: File,
        beginSize: Long,
        endSize: Long
    ): Boolean {
        var waitSize = endSize - beginSize
        val out = FileOutputStream(localFile, true)
        ftpClient!!.restartOffset = beginSize
        val input = ftpClient!!.retrieveFileStream(String(remote.toByteArray(Charsets.ISO_8859_1)))
        val byteArray = ByteArray(1024)
        var c = 0
        var finishSize = 0.0
        var finishPercent: Double = 0.0
        while (input.read(byteArray).also { c = it } != -1) {
            out.write(byteArray, 0, c)
            finishSize += c

            if ((finishSize / waitSize) - finishPercent > 0.01) {
                finishPercent = finishSize / waitSize
                val currentSize = java.math.BigDecimal("${finishSize / 1024 / 1024}")
                    .setScale(2, BigDecimal.ROUND_HALF_DOWN).toDouble()
                val percent = (finishPercent * 100).toInt()
                downLoadListener?.onDownLoadListener(percent, currentSize)
              
            } else if (finishSize - waitSize == 0.0) {
                finishPercent = finishSize / waitSize
                val currentSize = java.math.BigDecimal("${finishSize / 1024 / 1024}")
                    .setScale(2, BigDecimal.ROUND_HALF_DOWN).toDouble()
                val percent = (finishPercent * 100).toInt()
                downLoadListener?.onDownLoadListener(percent, currentSize)
              
            }
        }
        input.close()
        out.close()
        return ftpClient!!.completePendingCommand()
    }

这里remote是远程文件目录,localfile是本地文件,beginsize就是文件传输开始位置,endsize是结束位置。

最后,定义进度监听接口:

interface FtpProcessListener {
        fun onDownLoadListener(finishPercent: Int, finishSize: Double)
    }

使用方法:

我是直接在需要检查更新的activity中,实现进度监听的接口

override fun initView(){
FtpUtil.setListener(this)
} 

在协程中,连接ftp服务器,通过远程文件名检查是否需要更新并做后续处理

CoroutineScope(Dispatchers.IO).launch {
                val res = FtpUtil.ftpConnect()
                if (res) {
                    val list = FtpUtil.listFiles()
                    var buildCode = 0
                    var fileName: String = "base.apk"
                    var filePath =
                        Environment.getExternalStorageDirectory().path + File.separator + APP.missdyFile
                    var ftpFile: FTPFile? = null
                    list.forEach {
                        ftpFile = it
                        val apkInfo = it.name.split("-")
                        buildCode = apkInfo[1].toInt()
                        fileName = it.name
                        totalSize = java.math.BigDecimal("${it.size / 1024.0 / 1024.0}")
                            .setScale(2, BigDecimal.ROUND_HALF_DOWN).toDouble()
                        Log.i("FTPUtil", "${it.name},${totalSize}MB")

                    }
                    withContext(Dispatchers.Main) {
                        LoadingDialog.closeDialog(loadingDialog)
                        if (buildCode > versionCode!!) {
                            val inflate =
                                this@AboutActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
                            val contentView = inflate.inflate(R.layout.dialog_reset, null)
                            val messageView = contentView.findViewById<TextView>(R.id.tv_message)
                            messageView.text = resources.getString(R.string.label_ready_for_update)
                            val positiveListener = View.OnClickListener {
                                mDialog?.dismiss()
                                showProgressDialog()
                                Thread {
                                    val file = File("$filePath/$fileName")
                                    if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
                                        if (!file.parentFile.exists()) {
                                            file.parentFile.mkdirs()
                                        }
                                        val res = FtpUtil.download(file, ftpFile!!)
                                        if (res) {
                                            FtpUtil.disConnect()
                                            FileUtil.install(file, this@AboutActivity)
                                        }
                                    }

                                }.start()

                            }
                            val cancelListener = View.OnClickListener {
                                mDialog?.dismiss()
                            }
                            showDialog(
                                "",
                                contentView,
                                positiveListener,
                                cancelListener,
                                resources.getString(R.string.label_no),
                                resources.getString(R.string.label_yes)
                            )
                        } else {
                            ToastUtil.showToast(resources.getString(R.string.label_already_up_to_date))
                        }
                    }
                }
            }

再贴一下进度条dialog的代码

首先是布局:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/box_white_30"
    android:minWidth="719dp"
    android:minHeight="280dp">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="48dp"
        android:textColor="@color/black"
        android:textSize="40sp"
        android:textStyle="bold"
        android:visibility="gone"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="48dp"
        android:text="@string/label_download"
        android:textColor="@color/black"
        android:textSize="40sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_goneMarginTop="20dp" />

    <ProgressBar
        android:id="@+id/progressBar"
        style="@style/progressBarHorizontal"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:layout_marginStart="20dp"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="20dp"
        android:max="100"
        android:progress="0"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_message" />

    <TextView
        android:id="@+id/progress_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="15dp"
        android:gravity="right"
        android:padding="2dp"
        android:text="0MB/-MB"
        android:textColor="@color/black"
        android:textSize="28sp"
        app:layout_constraintEnd_toEndOf="@id/progressBar"
        app:layout_constraintStart_toStartOf="@id/progressBar"
        app:layout_constraintTop_toBottomOf="@id/progressBar" />

</androidx.constraintlayout.widget.ConstraintLayout>

progress的样式:

//drawable
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background">
        <shape>
            <solid android:color="@color/gray" />
            <corners android:radius="25dp" />
        </shape>
    </item>
    <item android:id="@android:id/progress">
        <clip>
            <shape>
                <solid android:color="#1E95D2" />
                <corners android:radius="25dp" />
            </shape>
        </clip>
    </item>
</layer-list>

//style
<style name="progressBarHorizontal" parent="android:Widget.ProgressBar.Horizontal">
        <item name="android:indeterminateOnly">false</item>
        <item name="android:progressDrawable">@drawable/progress_style</item>
        <item name="android:minHeight">15dp</item>
        <item name="android:maxHeight">15dp</item>
    </style>

dialog:

class ProgressDialog : Dialog {
    constructor(context: Context) : super(context, R.style.CustomDialog)

    private var progressBar: ProgressBar? = null
    private var progressTextView: TextView? = null

    fun setProgress(progress: Int, progressText: String) {
        progressBar?.progress = progress
        progressTextView?.text = progressText
    }

    class Builder(context: Context) {
        private var title: String? = null
        private var msg: String? = null
        private val layout: View
        private val dialog: ProgressDialog = ProgressDialog(context)


        init {
            val inflate =
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
            layout = inflate.inflate(R.layout.dialog_process, null)
            dialog.addContentView(
                layout,
                ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT
                )
            )
        }

        fun setTitle(title: String?): Builder {
            this.title = title
            return this
        }

        fun setMessage(msg: String?): Builder {
            this.msg = msg
            return this
        }

        fun createProgressDialog(): ProgressDialog {
            dialog.progressBar = layout.findViewById(R.id.progressBar)
            dialog.progressTextView = layout.findViewById(R.id.progress_text)
            create()
            return dialog
        }

        private fun create() {
            if (msg != null) {
                layout.findViewById<TextView>(R.id.tv_message).visibility = View.VISIBLE
                layout.findViewById<TextView>(R.id.tv_message).text = msg
            }
            if (title != null) {
                layout.findViewById<TextView>(R.id.tv_title).visibility = View.VISIBLE
                layout.findViewById<TextView>(R.id.tv_title).text = title
            }

            dialog.setContentView(layout)
            dialog.setCancelable(false)
            dialog.setCanceledOnTouchOutside(false)
        }
    }
}

在activity中使用

var mProgressDialog: ProgressDialog? = null
    private fun showProgressDialog() {
        mProgressDialog = ProgressDialog.Builder(this)
            .setTitle(null)
            .setMessage(resources.getString(R.string.label_download))
            .createProgressDialog()
        val dialogWindow = mProgressDialog!!.window
        val lp = dialogWindow!!.attributes
        lp?.width = WindowManager.LayoutParams.WRAP_CONTENT
        lp?.height = WindowManager.LayoutParams.WRAP_CONTENT
        dialogWindow.attributes = lp
        mProgressDialog!!.window!!.setFlags(
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
        )
        mProgressDialog!!.setCanceledOnTouchOutside(false)
        DialogUtils.hideBottomNav(mProgressDialog)
        mProgressDialog!!.show()
    }

下载的时候更新进度:

 override fun onDownLoadListener(finishPercent: Int, finishSize: Double) {
        runOnUiThread {
            mProgressDialog?.setProgress(finishPercent, "${finishSize}/${totalSize}MB")
            if (finishPercent == 100) {
                mProgressDialog?.dismiss()
            }
        }
    }

apk下载完成后安装:

fun install(apkFile: File, context: Context?) {
        val intent = Intent(Intent.ACTION_VIEW)
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            val contentUri = FileProvider.getUriForFile(
                context!!,
                "${context.packageName}.FileProvider",
                apkFile
            )
            intent.setDataAndType(contentUri, "application/vnd.android.package-archive")
        } else {
            intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive")
        }
        context!!.startActivity(intent)
    }

在AndroidManifest.xml中添加provider

<provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.zxl.missdy.FileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths" />
        </provider>

以上只是一个实现思路,并不能直接拿来用,需自行进行修改,不管文件上传还是下载,道理都是相同的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值