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 } }