zoukankan      html  css  js  c++  java
  • 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
        }
    }
  • 相关阅读:
    超链接导航栏案例
    css中定位功能的特性
    background的使用
    数据库单表查询
    各种算法
    面向对象能做什么和特性及嵌套
    c3算法详解
    面向对象继承
    生成器
    迭代器
  • 原文地址:https://www.cnblogs.com/aimqqroad-13/p/9598969.html
Copyright © 2011-2022 走看看