一、开门见山

代码

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

App自动更新(DownloadManager下载器)的更多相关文章

  1. Android学习系列(3)--App自动更新之自定义进度视图和内部存储

    友好的视觉感知和稳定的不出错表现,来自于我们追求美感和考虑的全面性,博客园从技术的角度,一直我都很欣赏.这篇文章是android开发人员的必备知识,是我特别为大家整理和总结的,不求完美,但是有用. 这 ...

  2. H5+app -- 自动更新

    一.最近做了一个app自动更新功能,用的基本都是网上找得到的. 1.h5+ 规范 :  http://www.html5plus.org/doc/zh_cn/maps.html 2.环形进度条插件:h ...

  3. Web APP自动更新

    我们的手机软件每天都要经营,经常需要更新,比如程序的Bug,好的功能,好的洁面... ... 这就需要我们的用户打开web app时候自动更新客户端程序,而不是再去应用程序商店从新下载.今天的笔记就是 ...

  4. web app升级—带进度条的App自动更新

    带进度条的App自动更新,效果如下图所示:   技术:vue.vant-ui.5+ 封装独立组件AppProgress.vue: <template> <div> <va ...

  5. 解决“iOS 7 app自动更新,无法在app中向用户展示更新内容”问题

    转自cocoachina iOS 7能在后台自动app,这对开发者来说和用户都很方便,但是还是有一些缺点.用户不会知道app本次更新的内容,除非他们上到app的App Store页面去查看.开发者也会 ...

  6. 使用Mac App Store更新、下载软件时出现未知错误的解决方法

    很多果迷在使用 Mac App Store 更新/下载软件时,可能都曾被”未知错误”困扰过,怎么解决也不行.然而,过一段时间不知道做了什么又自己好了.今天我们提供两个解决这个问题的方法,下次遇到这个问 ...

  7. Android 云服务器的搭建和友盟APP自动更新功能的实现

    setContentView(R.layout.activity_splash); //Bmob SDK初始化--只需要这一段代码即可完成初始化 //请到Bmob官网(http://www.bmob. ...

  8. Android App自动更新解决方案(DownloadManager)

    一开始,我们先向服务器请求数据获取版本 public ObservableField<VersionBean> appVersion = new ObservableField<&g ...

  9. App自动更新之通知栏下载

    见证过博客园的多次升级,你也希望你的软件通过更新发布新特性通知用户吧,是的.这篇文章是android开发人员的必备知识,是我特别为大家整理和总结的,不求完美,但是有用. 1.设计思路,使用Versio ...

随机推荐

  1. Luogu 1086 - 花生采摘 - [简单模拟]

    题目链接:https://www.luogu.org/problemnew/show/P1086 题目描述鲁宾逊先生有一只宠物猴,名叫多多.这天,他们两个正沿着乡间小路散步,突然发现路边的告示牌上贴着 ...

  2. 浅谈Vue.use

    我们先来看一个简单的事例首先我使用官方脚手架新建一个项目vue init webpack vue-demo然后我创建两个文件index.js plugins.js.我将这两个文件放置在src/clas ...

  3. webstorm 介绍

    最新版2017 破解 注册时,在打开的License Activation窗口中选择“License server”,在输入框输入下面的网址: http://idea.iteblog.com/key. ...

  4. OA流程分析

    OA流程分析: 1. 流程定义:可视化可拖拽的流程环节设置,流程定义完成后保存在数据表中,字段有流程ID,名称,流程流转环节. 2. 画业务表单,新建业务数据表. 3. 表单数据填好后,启动流程:

  5. list集合排序

    https:/blog.csdn.net/veryisjava/article/details/51675036 public static void main(String[] args) { Li ...

  6. 转:解决tomcat服务器跨域问题

    原文地址: 解决tomcat服务器跨域请求问题 注:还未测试 在tomcat 的web.xml 配置文件中加入如下配置过滤器 (如web.xml中有多个filter时要把下面配置放在最前端) < ...

  7. Cesium 实践

    详细内容请参考教程:https://www.jianshu.com/p/31c3b55a21eb 该教程翻译自官方英文教程,对入门cesium 帮助很大. 2,Cesium项目实例    实践: 问题 ...

  8. Grafana 安装及 Windows 应用程序服务配置工具 NSSM使用

    网址:https://blog.csdn.net/kk185800961/article/details/83515382 1.进入conf文件,将 defaults.ini 复制一份,命名为cust ...

  9. java框架之SpringBoot(6)-Restful风格的CRUD示例

    准备 环境 IDE:Idea SpringBoot版本:1.5.19 UI:BootStrap 4 模板引擎:thymeleaf 3 效果:Restful 风格 CRUD 功能的 Demo 依赖 &l ...

  10. ubuntu 安装完后对于开发需要做的事情

    是从 https://www.osboxes.org/ubuntu/ 下载的vdi文件,估计vmware对应的应该也有. 1. 安装 openssh-server apt-get install op ...