最近做的一个小需求,通过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>
以上只是一个实现思路,并不能直接拿来用,需自行进行修改,不管文件上传还是下载,道理都是相同的。