uniapp实现Android apk自动检测更新强制下载安装

概要

本篇文章主要讲诉uniapp 打包的APK如何实现在线升级功能,实现强制升级更新、可选升级更新、下载进度百分比显示、下载完成自动跳转安装功能,自己项目已经测试没有问题,可以结合自己的项目进行引入使用。

需求分析

1、需要进入app进行检查版本,进行判断是否需要更新

2、显示升级更新弹窗

3、是强制升级更新或者可选升级更新

4、对APP下载监听,进行进度条的展示(plus.downloader.createDownload(url,options,completedCallback)(下载))

5、下载完成apk后,直接跳转安装 (plus.runtime.install(安装APP))

技术实现梳理

1.是否更新判断:

通过接口(自己实现接口)获取线上最新版本号(默认规定版本号为正整数)与本地APP版本号进行比较大小,当线上最新版本号大于本地版本号就需要更新。本地App版本可在每次发版时候在manifest.json-基础配置-应用版本号进行设置

2.升级弹窗的展示

升级弹窗实现有2种方案,一种直接在首页里嵌套弹窗组件,另一种是把弹窗放置在独立的页面,并把页面窗口设置透明,当需要升级的时候直接从首页进入,从视觉效果上看就相当于在首页上的悬浮窗口。考虑到后续有强制更新页面不能返回等操作,便于维护本案例将采用第二种方案

3.根据升级类型限制操作

当升级类型为强制升级意味着页面不能做除了升级的任何操作,包括返回功能,关闭弹窗功能,禁止返回可通过onBackPress生命周期函数处理,弹窗关闭入口动态控制,包括关闭按钮,遮罩层点击关闭功能等

4.下载APP监听下载进度

通过H5+方式下载 :plus.downloader.createDownload,生成下载任务对象(downloadTask),通过downloadTask.addEventListener("statechanged",(task,status)=>{})监听下载进度

5.下载完自动安装

通过H5+ plus.runtime.install实现自动安装,该api只能监听是否打开安装页面,无法监测到apk是否安装成功,还需要调用安卓原生注册广播事件,监听apk安装成功回调

核心API讲解

1.plus.downloader.createDownload(url,options,completedCallback)(下载)

说明:请求下载管理创建新的下载任务,创建成功则返回Download对象,用于管理下载任务

参数:url: ( String ) 必选 要下载文件资源地址

要下载文件的url地址,仅支持网络资源地址,支持http或https协议。 允许创建多个相同url地址的下载任务。 注意:如果url地址中包含中文或空格等,需要进行urlencode转换。

options: ( DownloadOptions ) 可选 下载任务的参数

可通过此参数设置下载任务属性,如保存文件路径、下载优先级等。

completedCallback: ( DownloadCompletedCallback ) 可选 下载任务完成回调函数

当下载任务下载完成时触发,成功或失败都会触发。

返回值:Download:新建的下载任务对象

2.plus.runtime.install(filePath,options,installSuccessCB,installErrorCB)(安装APP)

说明:支持以下类型安装包:1. 应用资源安装包(wgt),扩展名为'.wgt'; 2. 应用资源差量升级包(wgtu),扩展名为'.wgtu'; 3. 系统程序安装包(apk),要求使用当前平台支持的安装包格式。 注意:仅支持本地地址,调用此方法前需把安装包从网络地址或其他位置放置到运行时环境可以访问的本地目录。

参数:

filePath: ( String ) 必选 要安装的文件路径

支持应用资源安装包(wgt)、应用资源差量升级包(wgtu)、系统程序包(apk)。

options: ( WidgetOptions ) 可选

应用安装设置的参数

installSuccessCB: ( InstallSuccessCallback ) 可选

正确安装后的回调

installErrorCB: ( InstallErrorCallback ) 可选

安装失败的回调

返回值:void : 无

代码实现

新增升级弹窗页面路由,设置窗口透明

项目目录:

pages.json:

javascript 复制代码
{   
	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
    {
        "path": "pages/index/index", //首页
        "style": {
            "navigationBarTitleText": ""
        }
    },{
        "path": "pages/index/upgrade", //升级窗口页面
        "style": {
            "navigationBarTitleText": "",
            "navigationStyle": "custom", //导航栏自定义
            "app-plus": {
                "bounce": "none",
                "animationType": "none", //取消窗口动画
                "background": "transparent" // 设置背景透明
            }
        }
    }
    ],
    "globalStyle": {
        "navigationBarTextStyle": "black",
        "navigationBarTitleText": "uni-app",
        "navigationBarBackgroundColor": "#F8F8F8",
        "backgroundColor": "#F8F8F8"
    },
    "uniIdRouter": {}
}
index.vue:(首页)

从接口获取最新版本号,跟本地对比,判断是否进入升级弹窗页

javascript 复制代码
checkVersionPage(){
				var postData = {
					app: getApp().globalData.APP,
					version: getApp().globalData.versionName
				}
				getAppVersion(postData).then(res=>{//这里是自己的接口进行判断,获取本地版本名称和最新版本apk版本名称做的对比,服务端做的对比
					if(res.data.code == '0000'){
						if(res.data.data.is_update == 2){
							var versionData = res.data.data
							uni.navigateTo({
								url: '/pages/upgrade/upgrade?versionName=' + res.data.data.version + '&isForce=' + res.data.data.is_force,
								events: {
									// 为指定事件添加一个监听器,获取被打开页面传送到当前页面的数据
									acceptDataFromOpenedPage: function(data) {
									},
									someEvent: function(data) {
									}
								},
								success: function(res) {
									// 通过eventChannel向被打开页面传送数据
									res.eventChannel.emit('acceptDataFromOpenerPage', versionData)
								},
								fail: function(error) {
								}
							})
						}
					}
				})
			},
upgrade.vue(升级弹窗页):

布局样式可根据实际调整,顶部背景图upgrade_bg.png自行放入static

javascript 复制代码
<template>
	<view class="upgrade-popup">
		<image class="header-bg" src='../../static/versionBac.jpg' mode="widthFix"></image>
		<view class="main">
			<view class="versionPopupTitleClass">发现新版本</view>
			<view class="versionPopupNameClass">v{{versionName}}</view>
			<view class="content">
				<text class="updataContentTitleClass">更新内容</text>
				<view class="updataContentClass" v-html="versionDesc"></view>
			</view>
			<!--下载状态-进度条显示 -->
			<view class="footer" v-if="isStartDownload">
				<view class="progress-view" :class="{'active':!hasProgress}" @click="handleInstallApp">
					<!-- 进度条 -->
					<view v-if="hasProgress" style="height: 100%;">
						<view class="txt">{{percentText}}</view>
						<view class="progress" :style="setProStyle"></view>
					</view>
					<view v-else>
						<view class="btn upgrade force">
							{{ isDownloadFinish  ? '立即安装' :'下载中...'}}
						</view>
					</view>
				</view>
			</view>
			<!-- 强制更新 -->
			<view class="footer" v-else-if="isForceUpdate">
				<view class="btn upgrade force" @click="handleUpgrade">立即体验</view>
			</view>
			<!-- 可选择更新 -->
			<view class="footer" v-else>
				<view class="btn close" @click="handleClose">暂不更新</view>
				<view class="btn upgrade" @click="handleUpgrade">立即体验</view>
			</view>
		</view>
	</view>
</template>
 
<script>
	import {
		downloadApp,
		installApp
	} from '../../utils/upgrade.js'
	export default {
 
		data() {
			return {
				isForceUpdate: false, //是否强制更新
				versionName: '', //版本名称
				versionDesc: '', //更新说明
				downloadUrl: '', //APP下载链接
				isDownloadFinish: false, //是否下载完成
				hasProgress: false, //是否能显示进度条
				currentPercent: 0, //当前下载百分比
				isStartDownload: false, //是否开始下载
				fileName: '', //下载后app本地路径名称
			}
		},
 
		computed: {
			//设置进度条样式,实时更新进度位置
			setProStyle() {
				return {
					width: (510 * this.currentPercent / 100) + 'rpx' //510:按钮进度条宽度
				}
			},
 
			//百分比文字
			percentText() {
				let percent = this.currentPercent;
				if (typeof percent !== 'number' || isNaN(percent)) return '下载中...'
				if (percent < 100) return `下载中${percent}%`
				return '立即安装'
			}
 
		},
 
		onLoad(option) {
			if(option.versionName){
				this.versionName = option.versionName
			}
			if(option.isForce == 2){
				this.isForceUpdate = true
			}else{
				this.isForceUpdate = false
			}
			var that = this
			let eventChannel = this.getOpenerEventChannel();
			try {
				// 监听versionUpdateData事件,获取上一页面通过eventChannel传送到当前页面的数据
				eventChannel.on('acceptDataFromOpenerPage', function(data) {
					that.$utils.log("打印传递过来的数据  " + JSON.stringify(data))
					that.init(data)
				})
			} catch (e) {
				//TODO handle the exception
			}
		},
 
		onBackPress(options) {
			// 禁用返回
			if (options.from == 'backbutton') {
				return true;
			}
		},
 
		methods: {
			//初始化获取最新APP版本信息
			init(data) {
				if (data.is_force == 2) {
					this.isForceUpdate = true
				} else {
					this.isForceUpdate = false
				}
				if (data.version) {
					this.versionName = data.version
				}
				if (data.version_desc) {
					this.versionDesc = data.version_desc
				}
				this.downloadUrl = data.url ? data.url : "xx.apk"
			},
 
			//更新
			handleUpgrade() {
				if (this.downloadUrl) {
					this.isStartDownload = true
					//开始下载App
					downloadApp(this.downloadUrl, current => {
						//下载进度监听
						this.hasProgress = true
						this.currentPercent = current
					}).then(fileName => {
						//下载完成
						this.isDownloadFinish = true
						this.fileName = fileName
						if (fileName) {
							//自动安装App
							this.handleInstallApp()
						}
					}).catch(e => {
						console.log(e, 'e')
					})
				} else {
					uni.showToast({
						title: '下载链接不存在',
						icon: 'none'
					})
				}
			},
 
			//安装app
 
			handleInstallApp() {
				//下载完成才能安装,防止下载过程中点击
				if (this.isDownloadFinish && this.fileName) {
					installApp(this.fileName, () => {
						//安装成功,关闭升级弹窗
						uni.navigateBack()
					})
				}
			},
 
			//关闭返回
			handleClose() {
				uni.setStorageSync('upDateTime',Math.floor(new Date().getTime() / 1000))
				uni.setStorageSync('isUpdate',true)
				uni.navigateBack()
			},
		}
	}
</script>
 
<style>
	page {
		background: rgba(0, 0, 0, 0.5);
		/**设置窗口背景半透明*/
	}
</style>
 
<style lang="scss" scoped>
	.upgrade-popup {
		width: 610rpx;
		height: 654.14rpx;
		position: fixed;
		top: 50%;
		left: 52%;
		transform: translate(-50%, -50%);
		border-radius: 20rpx;
		box-sizing: border-box;
	}
 
	.header-bg {
		width: 100%;
	}
	.main {
		position: absolute;
		top: 69rpx;
		padding-left: 35rpx;
		box-sizing: border-box;
		
 
		.footer {
			width: 100%;
			display: flex;
			justify-content: center;
			align-items: center;
			position: relative;
			flex-shrink: 0;
			margin-top: 100rpx;
 
			.btn {
				width: 246rpx;
				display: flex;
				justify-content: center;
				align-items: center;
				position: relative;
				z-index: 999;
				height: 96rpx;
				box-sizing: border-box;
				font-size: 38rpx;
				font-family: Source Han Sans-Regular;
				border-radius: 10rpx;
				letter-spacing: 2rpx;
				border-radius: 50rpx 50rpx 50rpx 50rpx;
				&.force {
					width: 500rpx;
				}
 
				&.close {
					margin-right: 25rpx;
					background: #E1E1E1;
					font-weight: 400;
					font-size: 34rpx;
					color: #3D3D3D;
				}
 
				&.upgrade {
					background: linear-gradient( 90deg, #FF470B 1%, #FC966E 100%);
					color: white;
				}
 
			}
 
			.progress-view {
				width: 500rpx;
				height: 90rpx;
				display: flex;
				position: relative;
				align-items: center;
				border-radius: 50rpx;
				background-color: #FFCEBE;
				display: flex;
				justify-content: flex-start;
				padding: 0px;
				box-sizing: border-box;
				border: none;
				overflow: hidden;
				font-size: 38rpx;
				font-family: Source Han Sans-Regular;
				&.active {
					// background-color: #FFCEBE;
					background: linear-gradient( 90deg, #FF470B 1%, #FC966E 100%);
					border-radius: 50rpx 50rpx 50rpx 50rpx;
				}
				.progress {
					height: 100%;
					// background-color: #FFCEBE;
					background: linear-gradient( 90deg, #FF470B 1%, #FC966E 100%);
					padding: 0px;
					box-sizing: border-box;
					border: none;
					border-radius: 50rpx;
				}
 
				.txt {
					
					position: absolute;
					top: 50%;
					left: 50%;
					transform: translate(-50%, -50%);
					color: #fff;
 
				}
 
			}
 
		}
 
	}
	.content {
		margin-top: 60rpx;
	}
	.versionPopupTitleClass{
		height: 53rpx;
		font-family: Alibaba PuHuiTi;
		font-weight: 700;
		font-size: 38rpx;
		color: #3D3D3D;
		line-height: 53rpx;
		text-align: left;
		font-style: normal;
		text-transform: none;
		margin-top: 41.35rpx;
	}
	.versionPopupNameClass{
		width: 71rpx;
		height: 38rpx;
		font-family: Source Han Sans-Regular;
		font-weight: 350;
		font-size: 26rpx;
		color: #ADADAD;
		line-height: 38rpx;
		text-align: left;
		font-style: normal;
		text-transform: none;
		margin-top: 8rpx;
	}
	.updataContentTitleClass{
		height: 38rpx;
		font-family: Source Han Sans-Regular;
		font-weight: 400;
		font-size: 26rpx;
		color: #3D3D3D;
		line-height: 38rpx;
		text-align: left;
		font-style: normal;
		text-transform: none;
		margin-top: 120rpx;
	}
	.updataContentClass{
		height: 32rpx;
		font-family: Source Han Sans-Regular;
		font-weight: 400;
		font-size: 23rpx;
		color: #727272;
		line-height: 32rpx;
		text-align: left;
		font-style: normal;
		text-transform: none;
		margin-top: 16.92rpx;
	}
	.zanbugengxinClass{
		width: 252rpx;
		height: 92rpx;
		background: #E1E1E1;
		border-radius: 50rpx 50rpx 50rpx 50rpx;
		font-family: Source Han Sans-Regular;
		font-weight: 400;
		font-size: 34rpx;
		color: #3D3D3D;
		text-align: center;
		font-style: normal;
		text-transform: none;
	}
	.lijitiyanClass{
		width: 252rpx;
		height: 92rpx;
		background: linear-gradient( 90deg, #FF470B 1%, #FC966E 100%);
		border-radius: 50rpx 50rpx 50rpx 50rpx;
		font-family: Source Han Sans-Regular;
		font-weight: 400;
		font-size: 34rpx;
		color: #FFFFFF;
		text-align: center;
		font-style: normal;
		text-transform: none;
	}
</style>
upgrade.js(下载、安装工具类):
javascript 复制代码
/**
 * @description H5+下载App
 * @param downloadUrl:App下载链接
 * @param progressCallBack:下载进度回调
 */
export const downloadApp = (downloadUrl, progressCallBack = () => {}, ) => {
	return new Promise((resolve, reject) => {
		//创建下载任务
		const downloadTask = plus.downloader.createDownload(downloadUrl, {
			method: "GET"
		}, (task, status) => {
			console.log(status, 'status')
			if (status == 200) { //下载成功
				resolve(task.filename)
			} else {
				reject('fail')
				uni.showToast({
					title: '下载失败',
					duration: 1500,
					icon: "none"
				});
			}
		})
		//监听下载过程
		downloadTask.addEventListener("statechanged", (task, status) => {
			switch (task.state) {
				case 1: // 开始  
					break;
				case 2: //已连接到服务器  
					break;
				case 3: // 已接收到数据  
					let hasProgress = task.totalSize && task.totalSize > 0 //是否能获取到App大小
					if (hasProgress) {
						let current = parseInt(100 * task.downloadedSize / task
						.totalSize); //获取下载进度百分比
						progressCallBack(current)
					}
					break;
				case 4: // 下载完成       
					break;
			}
		});
 
		//开始执行下载
		downloadTask.start();
	})
}
 
/**
 * @description H5+安装APP
 * @param fileName:app文件名
 * @param callBack:安装成功回调
 */
export const installApp = (fileName, callBack = () => {}) => {
	//注册广播监听app安装情况
	onInstallListening(callBack);
	//开始安装
	plus.runtime.install(plus.io.convertLocalFileSystemURL(fileName), {}, () => {
		//成功跳转到安装界面
	}, function(error) {
		uni.showToast({
			title: '安装失败',
			duration: 1500,
			icon: "none"
		});
	})
}
 
/**
 * @description 注册广播监听APP是否安装成功
 * @param callBack:安装成功回调函数
 */
const onInstallListening = (callBack = () => {}) => {
	let mainActivity = plus.android.runtimeMainActivity(); //获取activity
	//生成广播接收器
	let receiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', {
		onReceive: (context, intent) => { //接收广播回调  
			plus.android.importClass(intent);
			mainActivity.unregisterReceiver(receiver); //取消监听
			callBack()
		}
	});
	let IntentFilter = plus.android.importClass('android.content.IntentFilter');
	let Intent = plus.android.importClass('android.content.Intent');
	let filter = new IntentFilter();
	filter.addAction(Intent.ACTION_PACKAGE_ADDED); //监听APP安装     
	filter.addDataScheme("package");
	mainActivity.registerReceiver(receiver, filter); //注册广播
}

注意事项:

1.无法弹出安装APP界面

manifest.json-APP常用其他设置-targetSdkVersion必须设置26以上

2.无法安装APP

manifest.json-APP权限设置需勾选:

"<uses-permission android:name=\"android.permission.INSTALL_PACKAGES\"/>",

"<uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\"/>"

3.获取的版本号和设置的不一致

通过uni.getSystemInfoSync().appVersionCode获取的本地应用版本号和manifest.json-应用版本号设置不一致,需要云打包或者自定义基座里面才能生效

4.无法获取下载进度

app下载请求回复体头部需要返回content-length字段,才能正常获取到app总大小,需要下载接口开启支持,本演示例子已做显示的兼容处理。

相关推荐
sunly_30 分钟前
Flutter:页面中触发点击事件,通过id更新特定视图
前端·javascript·flutter
鱼大大博客4 小时前
Edge SCDN深度解析,边缘安全加速的创新实践
前端·安全·edge
始终奔跑在路上4 小时前
黑客基础之html
前端·网络·web安全·网络安全·html
web135956097055 小时前
【前端】HTML实现个人简历信息填写页面
前端·html
会编程的果子君5 小时前
数据分析系列----beautifulsoup4模块
服务器·前端·数据分析
耶啵奶膘6 小时前
uniapp——解决输入内容后跳转到下个页面 底部按钮不显示的问题
前端·uni-app
老大白菜6 小时前
基于Go和Python的高效Web开发实战解析
前端·python·golang
m0_748235957 小时前
前端性能优化面试题汇总
前端·性能优化
Williamoses7 小时前
挺详细的记录electron【V 33.2.0】打包vue3项目为可执行程序
前端·vue.js·electron