18034727885
2026-04-14 370014cbb9ec1ef64e7e42bb4f9a12671461d8e4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
package com.xw.common.net
 
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.Handler
import android.os.Looper
import com.xw.common.utils.LogUtils
import android.view.Gravity
import android.view.View
import android.view.WindowManager
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.view.isVisible
import com.blankj.utilcode.util.AppUtils
import com.hjq.permissions.OnPermissionCallback
import com.hjq.permissions.XXPermissions
import com.xw.common.R
import com.xw.common.base.BaseDialog
import com.xw.common.databinding.DialogUpdateTipsBinding
import com.xw.common.utils.ToastUtilCommon
import com.xw.common.utils.ToolUtil
import java.io.File
import java.io.FileOutputStream
import java.net.URL
 
/**
 * 检查更新版本工具类
 */
class UpdateManager {
 
    companion object {
        const val TAG = "zmc-UpdateManager"
        const val TIPS = "请授权访问存储空间权限,否则App无法更新"
 
        @SuppressLint("StaticFieldLeak")
        var updateManager: UpdateManager? = null
        val instance: UpdateManager?
            get() {
                if (updateManager == null) {
                    updateManager = UpdateManager()
                }
                return updateManager
            }
    }
 
    private val downLoadPath = lazy {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !Environment.isExternalStorageLegacy()) {
            (context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath
                ?: "") + "/download/"
        } else {
            val packageName = context.packageName
            Environment.getExternalStorageDirectory().absolutePath + "/" + packageName + "/download/"
        }
    }
    private val versionInfo = VersionInfo()
 
    private val oldApkName by lazy { "${AppUtils.getAppVersionName()}.apk" }
    lateinit var context: Context
 
    fun setContext(context: Context): UpdateManager {
        this.context = context
        return this
    }
 
    fun apkUrl(apkUrl: String): UpdateManager {
        versionInfo.apk = apkUrl
        return this
    }
 
    fun updateContent(content: String): UpdateManager {
        versionInfo.content = content
        return this
    }
 
    fun updateVersionName(version: String): UpdateManager {
        versionInfo.versionName = version
        return this
    }
 
    fun updateVersionCode(version: Int): UpdateManager {
        versionInfo.versionCode = version
        return this
    }
    fun updateForceUpdateFlag(forceUpdateFlag: Int): UpdateManager {
        versionInfo.forceUpdateFlag = forceUpdateFlag
        return this
    }
 
    fun show() {
        val versionCode = AppUtils.getAppVersionCode()
        if (versionCode < versionInfo.versionCode) showUpdateDialog()
 
    }
 
    private var updateDialog: UpdateDialog? = null
 
    /**
     * 版本更新提示框
     */
    private fun showUpdateDialog() {
        if (updateDialog != null && updateDialog!!.isShowing) return
        if (updateDialog == null) {
            updateDialog = UpdateDialog(context)
 
            updateDialog!!.binding!!.desc.text = versionInfo.content
            updateDialog!!.binding!!.cancel.isVisible = versionInfo.forceUpdateFlag != 1
 
            updateDialog!!.binding!!.cancel.setOnClickListener {
                updateDialog?.dismiss()
            }
            updateDialog!!.binding!!.download.setOnClickListener {
                updateApk()
            }
        }
        updateDialog!!.show()
    }
 
    /**
     * 检查有没有最新的APK
     */
    private fun checkIsHaveNewAPK(): Boolean {
        val file = File(downLoadPath.value)
        if (!file.exists()) {
            file.mkdirs()
        }
        val fileAPK = File(downLoadPath.value, "${versionInfo.versionName}.apk")
        return fileAPK.exists()
    }
 
    /**
     * 删除旧的apk
     */
    private fun delOldApk() {
        val fileAPK = File(downLoadPath.value, oldApkName)
        if (fileAPK.exists()) {
            fileAPK.delete()
        }
    }
 
    /**
     * 删除下载状态的包
     */
    private fun delDownloadStatusFile() {
        val fileAPK = File(downLoadPath.value, "${versionInfo.versionName}.apk.download")
        if (fileAPK.exists()) {
            fileAPK.delete()
        }
    }
 
    /**
     * 下载APK
     */
    private fun downloadApk() {
        updateDialog!!.binding!!.horizontalProgress.visibility = View.VISIBLE
        updateDialog!!.binding!!.download.visibility = View.GONE
        updateDialog!!.binding!!.cancel.visibility = View.GONE
        Thread {
            try {
                val startTime = System.currentTimeMillis()
                LogUtils.i(TAG, "startTime=$startTime")
                //获取文件名
                val myURL = URL(versionInfo.apk)
                val conn = myURL.openConnection()
                conn.connect()
                val `is` = conn.getInputStream()
                val fileSize = conn.contentLength //根据响应获取文件大小
                LogUtils.i(TAG, "总大小=$fileSize")
                if (fileSize <= 0) throw RuntimeException("无法获知文件大小 ")
                if (`is` == null) throw RuntimeException("stream is null")
                val file1 = File(downLoadPath.value)
                if (!file1.exists()) {
                    file1.mkdirs()
                }
                //把数据存入路径+文件名
                val fos =
                    FileOutputStream("${downLoadPath.value}/${versionInfo.versionName}.apk.download")
                val buf = ByteArray(1024)
                var downLoadFileSize = 0
                do {
                    //循环读取
                    val numread = `is`.read(buf)
                    if (numread == -1) {
                        break
                    }
                    fos.write(buf, 0, numread)
                    downLoadFileSize += numread
                    LogUtils.i(TAG, "downLoadFileSize=$downLoadFileSize")
                    LogUtils.i(TAG, "fileSize=$fileSize")
                    LogUtils.i(
                        TAG,
                        "download downLoadFileSize=" + (downLoadFileSize.toFloat() * 100 / fileSize).toInt()
                    )
                    Handler(Looper.getMainLooper()).post {
                        //更新进度条
                        updateDialog!!.binding!!.horizontalProgress.setProgress((downLoadFileSize.toFloat() * 100 / fileSize))
                    }
                } while (true)
                ToolUtil.copyFile(
                    "$downLoadPath/${versionInfo.versionName}.apk.download",
                    "$downLoadPath/${versionInfo.versionName}.apk"
                )
                delDownloadStatusFile()
                delOldApk()
                val fileAPK = File(downLoadPath.value, "${versionInfo.versionName}.apk")
                installApk(context, fileAPK)
                LogUtils.i(TAG, "download success")
                LogUtils.i(TAG, "totalTime=" + (System.currentTimeMillis() - startTime))
                `is`.close()
            } catch (ex: Exception) {
                LogUtils.e(TAG, "error: " + ex.message)
            }
        }.start()
    }
 
    /**
     * 安装apk
     */
    private fun installApk(context: Context, file: File) {
        val intent = Intent(Intent.ACTION_VIEW)
        //判断是否是AndroidN以及更高的版本
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            val contentUri =
                FileProvider.getUriForFile(context, context.packageName + ".fileprovider", file)
            intent.setDataAndType(contentUri, "application/vnd.android.package-archive")
        } else {
            intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive")
        }
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        context.startActivity(intent)
    }
 
    private fun updateApk() {
        //权限判断是否有访问外部存储空间权限
        if (Build.VERSION.SDK_INT < 23) {
            if (checkIsHaveNewAPK()) {
                // 安装
                val fileAPK = File(downLoadPath.value, "${versionInfo.versionName}.apk")
                installApk(context, fileAPK)
            } else {
                downloadApk()
            }
        } else {
            var permission =
                if (Build.VERSION.SDK_INT < 30) arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                else arrayOf(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
            val permissionList: MutableList<String> = ArrayList()
            for (permission in permission) {
                if (ContextCompat.checkSelfPermission(
                        context, permission
                    ) != PackageManager.PERMISSION_GRANTED
                ) {
                    permissionList.add(permission)
                }
            }
            if (permissionList.size > 0) {
                XXPermissions.with(context).permission(permission)
                    .request(object : OnPermissionCallback {
                        override fun onGranted(
                            permissions: MutableList<String>, allGranted: Boolean
                        ) { // 权限同意
                            if (checkIsHaveNewAPK()) {
                                // 安装
                                val fileAPK =
                                    File(downLoadPath.value, "${versionInfo.versionName}.apk")
                                installApk(context, fileAPK)
                            } else {
                                downloadApk()
                            }
                        }
 
                        override fun onDenied(
                            permissions: MutableList<String>, doNotAskAgain: Boolean
                        ) {
                            super.onDenied(permissions, doNotAskAgain)
                            //权限禁止
                            ToastUtilCommon.show(TIPS)
                        }
                    })
            } else { //  有权限
                if (checkIsHaveNewAPK()) {
                    // 安装
                    val fileAPK = File(downLoadPath.value, "${versionInfo.versionName}.apk")
                    installApk(context, fileAPK)
                } else {
                    downloadApk()
                }
            }
        }
    }
}
 
class UpdateDialog(context: Context) :
    BaseDialog<DialogUpdateTipsBinding>(context, R.style.LoadingDialog) {
    override fun getLayoutId() = R.layout.dialog_update_tips
 
    override fun initData() {
        window!!.setGravity(Gravity.CENTER)
        window!!.setLayout(
            WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT
        )
        setCancelable(false)
        setCanceledOnTouchOutside(false)
    }
}
 
data class VersionInfo(
    var apk: String = "", // 下载地址
    var content: String = "", // 版本描述
    var forceUpdateFlag: Int = 0, // 是否强制升级
    var versionCode: Int = 0, // 版本号
    var versionName: String = "" // 版本名
)