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
}
}
App自动更新(DownloadManager下载器)的更多相关文章
- Android学习系列(3)--App自动更新之自定义进度视图和内部存储
友好的视觉感知和稳定的不出错表现,来自于我们追求美感和考虑的全面性,博客园从技术的角度,一直我都很欣赏.这篇文章是android开发人员的必备知识,是我特别为大家整理和总结的,不求完美,但是有用. 这 ...
- H5+app -- 自动更新
一.最近做了一个app自动更新功能,用的基本都是网上找得到的. 1.h5+ 规范 : http://www.html5plus.org/doc/zh_cn/maps.html 2.环形进度条插件:h ...
- Web APP自动更新
我们的手机软件每天都要经营,经常需要更新,比如程序的Bug,好的功能,好的洁面... ... 这就需要我们的用户打开web app时候自动更新客户端程序,而不是再去应用程序商店从新下载.今天的笔记就是 ...
- web app升级—带进度条的App自动更新
带进度条的App自动更新,效果如下图所示: 技术:vue.vant-ui.5+ 封装独立组件AppProgress.vue: <template> <div> <va ...
- 解决“iOS 7 app自动更新,无法在app中向用户展示更新内容”问题
转自cocoachina iOS 7能在后台自动app,这对开发者来说和用户都很方便,但是还是有一些缺点.用户不会知道app本次更新的内容,除非他们上到app的App Store页面去查看.开发者也会 ...
- 使用Mac App Store更新、下载软件时出现未知错误的解决方法
很多果迷在使用 Mac App Store 更新/下载软件时,可能都曾被”未知错误”困扰过,怎么解决也不行.然而,过一段时间不知道做了什么又自己好了.今天我们提供两个解决这个问题的方法,下次遇到这个问 ...
- Android 云服务器的搭建和友盟APP自动更新功能的实现
setContentView(R.layout.activity_splash); //Bmob SDK初始化--只需要这一段代码即可完成初始化 //请到Bmob官网(http://www.bmob. ...
- Android App自动更新解决方案(DownloadManager)
一开始,我们先向服务器请求数据获取版本 public ObservableField<VersionBean> appVersion = new ObservableField<&g ...
- App自动更新之通知栏下载
见证过博客园的多次升级,你也希望你的软件通过更新发布新特性通知用户吧,是的.这篇文章是android开发人员的必备知识,是我特别为大家整理和总结的,不求完美,但是有用. 1.设计思路,使用Versio ...
随机推荐
- python全栈开发 * 18 面向对象知识点汇总 * 180530
18 面向对象初识1class person: level="高级动物" mind="有思想" def __init__(self,name,age,gent, ...
- C和C指针小记(十五)-结构和联合
1.结构 1.1 结构声明 在声明结构时,必须列出它包含的所有成员.这个列表包括每个成员的类型和名称. struct tag {member-list} variable-list; 例如 //A s ...
- 架构4(lvs lb集群解决方案二 lvs+keepalived)
keepalived 1.实现调度器的HA 2.对realserver做健康检测 3.动态维护ipvs路由表
- [qemu] 差分盘使用
我要装好多台同样配置的虚拟机.比如10台CentOS7, 各自有不同的配置. 这样的话装10台kvm虚拟机,特别的浪费空间,image文件相互迁移的话也不方便. 这个时候可以选择差分盘:就是先装一个标 ...
- web前端,多语言切换,data-localize,
demo: 链接:https://pan.baidu.com/s/1zhFHTv4P_epbBfpiggVDXg 提取码:aqts 要想有效果,必须发布在服务器上,可以在IIS上测试. 我只用到了中文 ...
- Spring Boot事务管理(中)
在上一篇 Spring Boot事务管理(上)的基础上介绍Spring Boot事务属性和事务回滚规则 . 4 Spring Boot事务属性 什么是事务属性呢?事务属性可以理解成事务的一些基本配置, ...
- Django进阶之QuerySet和中介模型
QuerySet QuerySet是查询集,就是传到服务器上的url里面的查询内容.其形态类似于Python的列表,列表中的元素是QuerySet对象.支持大部分列表的内置方法. 可切片 QueryS ...
- Hadoop生态集群MapReduce详解
一.概述 MapReduce是一种编程模型,这点很重要,仅仅是一种编程的模型,而不是具体的软件.在hadoop中,HDFS是分布式的文件存储系统,而MapReduce是一个分布式的计算框架.用于大规模 ...
- 【UML】NO.71.EBook.9.UML.4.002-【PowerDesigner 16 从入门到精通】- RQM
1.0.0 Summary Tittle:[UML]NO.71.EBook.9.UML.4.002-[PowerDesigner 16 从入门到精通]- RQM Style:DesignPatter ...
- Python subprocess.Popen() error (No such file or directory)
这个错误很容易引起误解,一般人都会认为是命令执行了,但是命令找不到作为参数对应的文件或者目录.其实还有一层含义,就是这个命令找不到,命令找不到,也会报没有这个文件或者目录的错误. 为什么找不到这个命令 ...