App自动更新(DownloadManager下载器)

一、开门见山

代码

object AppUpdateManager {
    const val APP_UPDATE_APK = "update.apk"
    private var builder: PgyUpdateManager.Builder? = null
    var dialog: AlertDialog? = null
    var downloadManager: DownloadManager? = null
    var downloadId: Long? = null

    @JvmStatic
    fun checkAppUpdateState(activity: Activity, isClose: Boolean) {
        if (isClose) {
            return
        }
        if (builder == null) {
            builder = PgyUpdateManager.Builder()
                    .setForced(true)                
                    .setUserCanRetry(false)        
                    .setDeleteHistroyApk(false)    
                    .setUpdateManagerListener(object : UpdateManagerListener {
                        override fun onNoUpdateAvailable() {
                        }

                        override fun onUpdateAvailable(appBean: AppBean) {
                            //有更新回调此方法
                            showUpdateDialog(activity, appBean)
                        }

                        override fun checkUpdateFailed(e: Exception) {
                        }
                    })
        }
        builder?.register()
    }

    @SuppressLint("InflateParams")
    private fun showUpdateDialog(activity: Activity, appBean: AppBean) {
        val view = LayoutInflater.from(activity).inflate(R.layout.layout_app_update, null)
        val ivCancel = view.findViewById<ImageView>(R.id.iv_cancel)
        val tvUpdateContent = view.findViewById<TextView>(R.id.tv_update_content)
        val pbProgress = view.findViewById<ProgressBar>(R.id.pb_download_progress)
        val startDownload = view.findViewById<TextView>(R.id.tv_start_download)
        dialog = AlertDialog.Builder(activity).setView(view).setCancelable(false).create()
        ivCancel.setOnClickListener {
            dialog?.dismiss()
            if (downloadId != null) {
                downloadManager?.remove(downloadId!!)
            }
        }
        tvUpdateContent.text = appBean.releaseNote
        startDownload.setOnClickListener {
            if (canDownloadState(activity)) {
                startDownload.isClickable = false
                startDownload.setOnClickListener(null)
                startDownload.text = "正在下载..."
                downloadApk(activity, appBean.downloadURL, pbProgress)
            } else {
                //打开浏览器
                startDownload.text = "打开浏览器下载"
                openBrowser(activity, appBean.downloadURL)
            }
        }
        dialog?.show()
    }

    private fun openBrowser(ctx: Context, downloadURL: String?) {
        dialog?.dismiss()
        val intent = Intent()
        intent.action = "android.intent.action.VIEW"
        val contentUrl = Uri.parse(downloadURL)
        intent.data = contentUrl
        ctx.startActivity(intent)
    }

    private fun downloadApk(context: Context, downloadUrl: String, pbProgress: ProgressBar) {
        val req = DownloadManager.Request(Uri.parse(downloadUrl))
        req.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI)
        req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
        req.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, APP_UPDATE_APK)
        // 设置一些基本显示信息
        req.setTitle("xxxxx")
        req.setDescription("下载完后请点击打开")
        req.setMimeType("application/vnd.android.package-archive")
        downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
        downloadId = downloadManager!!.enqueue(req)
        val query = DownloadManager.Query()
        pbProgress.max = 100
        val timer = Timer()
        val task = object : TimerTask() {
            override fun run() {
                val cursor = downloadManager!!.query(query.setFilterById(downloadId!!))
                if (cursor != null && cursor.moveToFirst()) {
                    val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
                    when (status) {
                        DownloadManager.STATUS_SUCCESSFUL -> {
                            pbProgress.progress = 100
                            installApk(context, context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).absolutePath + "/$APP_UPDATE_APK")
                            cancel()
                            dialog?.dismiss()
                        }
                        DownloadManager.STATUS_FAILED -> dialog?.dismiss()
                    }
                    val bytesDownloaded = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
                    val bytesTotal = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
                    val pro = (bytesDownloaded * 100) / bytesTotal
                    pbProgress.progress = pro
                }
                cursor.close()
            }

        }
        timer.schedule(task, 0, 1000)
    }

    fun installApk(context: Context, path: String) {
        val apkFile = File(path)
        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, "com.***.app.FileProvider", apkFile) //中间参数为 provider 中的 authorities
            intent.setDataAndType(contentUri, "application/vnd.android.package-archive")
        } else {
            intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive")
        }
        context.startActivity(intent)
    }

    @JvmStatic
    fun destroy() {
        builder = null
        dialog = null
        downloadManager = null
        downloadId = null
    }

    /**
     * 判断当前是否可以使用 DownloadManager
     * 有些国产手机会把 DownloadManager 进行阉割掉
     */
    private fun canDownloadState(ctx: Context): Boolean {
        try {
            val state = ctx.packageManager.getApplicationEnabledSetting("com.android.providers.downloads")
            if (state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED
                    || state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
                    || state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
                return false
            }
        } catch (e: Throwable) {
            e.printStackTrace()
            return false
        }
        return true
    }
}

  由于项目中的更新包是放在蒲公英上的,所以代码中不会有如何从服务器获取更新信息、版本号的对比判断更新等代码。大家从代码中只关注 拿到下载地址 到 完成安装这一个过程就可以了。下面我们就直接将适配吧。

二、更新中的适配

(1)DownloadManager的一点注意:

/**
     * 判断当前是否可以使用 DownloadManager
     * 有些国产手机会把 DownloadManager 进行阉割掉
     */
    private fun canDownloadState(ctx: Context): Boolean

 对于不能使用DownloadManager的特殊机型,在代码中我们打开手机浏览器去下载App更新包

if (canDownloadState(activity)) {
     downloadApk(activity, appBean.downloadURL, pbProgress)
} else {
     //打开浏览器
     openBrowser(activity, appBean.downloadURL)
}

(2)Android 7.0 访问手机本地文件(FileProvider)的适配

在代码中我们将下载的更新包放置在:setDestinationInExternalFilesDir 在源码中放置的地址就是 context.getExternalFilesDir(dirType) 。要关注这一点

req.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, APP_UPDATE_APK)

 我们获取本地更新包地址:

context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).absolutePath + "/$APP_UPDATE_APK"

在7.0及以上强制要转换一下这个地址,为了安全,否则就异常了。转换地址需要的步骤:

 1. 在 manifest 中加入一个 provider

<provider
       android:name="android.support.v4.content.FileProvider"
       android:authorities="${applicationId}.FileProvider"
       android:exported="false"
       android:grantUriPermissions="true">
       <meta-data
           android:name="android.support.FILE_PROVIDER_PATHS"
           android:resource="@xml/rc_file_path" />
</provider>

2. 需要一个 xml 文件 rc_file_path ,provider 中指定的

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <paths>
        <external-path name="camera_photos" path="" />
        <external-files-path name="name" path="" />
    </paths>
</resources>

如果要保证转换后能够正常安装,不会出现解析包异常,必须要做到的:

       ① 下载存放的地址和取的时候地址要一致

  ② 存放的路径要在 xml 中能够找到对应路径的 path。根据下表,所以代码中 xml 中定义了 <external-files-path> 节点

    对应关系如下:

节点 对应路径
<root-path> 代表设备的根目录 new File("/")
<files-path> 代表 context.getFileDir()
<cache-path> 代表 context.getCacheDir()
<external-path> 代表 Environment.getExternalStorageDirectory()
<external-files-path> 代表 context.getExternalFilesDirs()
<external-cache-path> 代表 getExternalCacheDirs()

(3)8.0 安装 App 权限

1.添加权限:

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

 manifest 如果没有这个权限,在8.0 手机上安装会失败,亲身经历,痛的领悟。

2. 在代码里面对权限进行处理

首先用canRequestPackageInstalls()方法判断你的应用是否有这个权限

haveInstallPermission = getPackageManager().canRequestPackageInstalls();

如果haveInstallPermission 为 true,则说明你的应用有安装未知来源应用的权限,你直接执行安装应用的操作即可。
如果haveInstallPermission 为 false,则说明你的应用没有安装未知来源应用的权限,则无法安装应用。由于这个权限不是运行时权限,所以无法再代码中请求权限,还是需要用户跳转到设置界面中自己去打开权限。

3. haveInstallPermission 为 false 的情况

跳转到未知来源应用权限管理列表:

Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
startActivityForResult(intent, 10086);

然后在onActivityResult中去接收结果:

if (resultCode == RESULT_OK && requestCode == 10086) {
     installProcess();//再次执行安装流程,包含权限判等
 }

更新:2019-04-19:

优化:自动更新,避免重复下载

1. 下载的Apk 文件命名,以 VersionCode 命名 例如 : xxx_1.0.1.apk

2. 获取最新版本信息,通过版本号组合出文件路径,判断本地是否有该安装包 apk 文件

3. 有该版本安装包直接点击安装,没有则下载安装。

object AppUpdateManager {
    private const val APP_UPDATE_APK = ".apk"
    private var builder: PgyUpdateManager.Builder? = null
    var dialog: AlertDialog? = null
    var downloadManager: DownloadManager? = null
    var downloadId: Long? = null
    var downloadVersion: String? = null

    @JvmStatic
    fun checkAppUpdateState(activity: Activity, isClose: Boolean) {
        if (isClose) {
            return
        }
        if (builder == null) {
            builder = PgyUpdateManager.Builder()
                .setForced(true)
                .setUserCanRetry(false)
                .setDeleteHistroyApk(false)
                .setUpdateManagerListener(object : UpdateManagerListener {
                    override fun onNoUpdateAvailable() {
                    }

                    override fun onUpdateAvailable(appBean: AppBean) {
                        downloadVersion = appBean.versionCode
                        //有更新回调此方法
                        showUpdateDialog(activity, appBean)
                    }

                    override fun checkUpdateFailed(e: Exception) {
                    }
                })
        }
        builder?.register()
    }

    @SuppressLint("InflateParams")
    private fun showUpdateDialog(activity: Activity, appBean: AppBean) {
        //判断是否已经下载
        val isDownloaded = isDownloaded(appBean, activity)
        val view = LayoutInflater.from(activity).inflate(R.layout.layout_app_update, null)
        val tvUpdateContent = view.findViewById<TextView>(R.id.tv_update_content)
        val pbProgress = view.findViewById<ProgressBar>(R.id.pb_download_progress)
        val startDownload = view.findViewById<TextView>(R.id.tv_start_download)
        startDownload.text = if (isDownloaded) "点击安装" else "下载更新"
        dialog = AlertDialog.Builder(activity).setView(view).setCancelable(false).create()
        tvUpdateContent.text = appBean.releaseNote
        startDownload.setOnClickListener {
            if (isDownloaded) {
                //已经下载过就直接安装
                installApk(activity, getApkPath(activity, appBean.versionCode))
            } else {
                if (canDownloadState(activity)) {
                    startDownload.isClickable = false
                    startDownload.setOnClickListener(null)
                    startDownload.text = "正在下载..."
                    downloadApk(activity, appBean, pbProgress)
                } else {
                    //打开浏览器
                    startDownload.text = "打开浏览器下载"
                    openBrowser(activity, appBean.downloadURL)
                }
            }
        }
        dialog?.show()
    }

    private fun isDownloaded(appBean: AppBean, activity: Activity): Boolean {
        val file = File(getApkPath(activity, appBean.versionCode))
        return file.exists()
    }

    private fun getApkPath(context: Context, versionCode: String): String {
        return context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/${getApkName(versionCode)}"
    }

    private fun getApkName(versionCode: String): String {
        return "spd_$versionCode$APP_UPDATE_APK"
    }

    private fun openBrowser(ctx: Context, downloadURL: String?) {
        dialog?.dismiss()
        val intent = Intent()
        intent.action = "android.intent.action.VIEW"
        val contentUrl = Uri.parse(downloadURL)
        intent.data = contentUrl
        ctx.startActivity(intent)
    }

    private fun downloadApk(context: Context, appBean: AppBean, pbProgress: ProgressBar) {
        //判断是否已经下载
        val req = DownloadManager.Request(Uri.parse(appBean.downloadURL))
        req.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI)
        req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
        req.setDestinationInExternalFilesDir(
            context, Environment.DIRECTORY_DOWNLOADS,
            getApkName(appBean.versionCode)
        )
        // 设置一些基本显示信息
        req.setTitle("spd-zs")
        req.setDescription("下载完后请点击打开")
        req.setMimeType("application/vnd.android.package-archive")
        downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
        downloadId = downloadManager!!.enqueue(req)
        val query = DownloadManager.Query()
        pbProgress.max = 100
        val timer = Timer()
        val task = object : TimerTask() {
            override fun run() {
                val cursor = downloadManager!!.query(query.setFilterById(downloadId!!))
                if (cursor != null && cursor.moveToFirst()) {
                    val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
                    when (status) {
                        DownloadManager.STATUS_SUCCESSFUL -> {
                            pbProgress.progress = 100
                            installApk(context, getApkPath(context, appBean.versionCode))
                            cancel()
                            dialog?.dismiss()
                        }
                        DownloadManager.STATUS_FAILED -> dialog?.dismiss()
                    }
                    val bytesDownloaded =
                        cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
                    val bytesTotal = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
                    val pro = (bytesDownloaded * 100) / bytesTotal
                    pbProgress.progress = pro
                }
                cursor.close()
            }

        }
        timer.schedule(task, 0, 1000)
    }

    fun installApk(context: Context, path: String) {
        val apkFile = File(path)
        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,
                    "com.bjknrt.handheld.FileProvider",
                    apkFile
                ) //中间参数为 provider 中的 authorities
            intent.setDataAndType(contentUri, "application/vnd.android.package-archive")
        } else {
            intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive")
        }
        context.startActivity(intent)
    }

    @JvmStatic
    fun destroy() {
        builder = null
        dialog = null
        downloadManager = null
        downloadId = null
    }

    /**
     * 判断当前是否可以使用 DownloadManager
     * 有些国产手机会把 DownloadManager 进行阉割掉
     */
    private fun canDownloadState(ctx: Context): Boolean {
        try {
            val state = ctx.packageManager.getApplicationEnabledSetting("com.android.providers.downloads")
            if (state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED
                || state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
                || state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED
            ) {
                return false
            }
        } catch (e: Throwable) {
            e.printStackTrace()
            return false
        }
        return true
    }
}
原文地址:https://www.cnblogs.com/aimqqroad-13/p/9598969.html