App自动更新(DownloadManager下载器)

时间:2023-03-09 02:14:57
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
}
}