正文
升级是应用最基本的功能,因为很少有一个应用发布后不在进行后期维护!
原生应用的升级比较常见,但是如今混合应用大热,因为项目,我就基于ionic框架实现了一个简单的升级,根据服务器端返回来确定强制还是非强制更新.
插件安装
file(访问文件)
ionic cordova plugin add cordova-plugin-file
npm install --save @ionic-native/file
File Transfer(上载和下载文件)
ionic cordova plugin add cordova-plugin-file-transfer
npm install --save @ionic-native/file-transfer
App Version(用来获取版本号)
ionic cordova plugin add cordova-plugin-app-version
npm install --save @ionic-native/app-version
Uid(获取设备标识,主要用于灰度升级,只升级用不到)
ionic cordova plugin add cordova-plugin-uid
npm install --save @ionic-native/uid
File Opener(打开下载完成的apk文件)
ionic cordova plugin add cordova-plugin-file-opener2
npm install --save @ionic-native/file-opener
Android Permissions(获取运行时权限)
ionic cordova plugin add cordova-plugin-android-permissions
npm install --save @ionic-native/android-permissions
升级服务
在src/app文件夹下创建NativeService.ts升级服务.
/**
* Created by llcn on 11-29.
* 升级模块
*/
import {Injectable} from '@angular/core';
import {Platform, AlertController} from 'ionic-angular';
import {AppVersion} from '@ionic-native/app-version';
import {File} from '@ionic-native/file';
import {FileTransfer, FileTransferObject} from "@ionic-native/file-transfer";
import {FileOpener} from '@ionic-native/file-opener';
import {Uid} from "@ionic-native/uid";
import {AndroidPermissions} from "@ionic-native/android-permissions";
import {ToastController} from 'ionic-angular';
import {HttpClient} from "@angular/common/http";
@Injectable()
export class NativeService {
constructor(private http: HttpClient,
private platform: Platform,
private alertCtrl: AlertController,
private transfer: FileTransfer,
private appVersion: AppVersion,
private file: File,
private fileOpener: FileOpener,
private uid: Uid,
private toastCtrl: ToastController,
private androidPermissions: AndroidPermissions) {
}
/**
* 检查app是否需要升级
*/
detectionUpgrade() {
//这里连接后台获取app最新版本号,然后与当前app版本号(this.getVersionNumber())对比
//版本号不一样就需要申请,不需要升级就return
this.getVersionNumber().then((version) => { // 获取版本号
this.getImei().then((imei) => { // 获取imei,用于灰度升级,有些需求不需要这一步
let body = {tag: 'update', data: {type: "chcnav", terminal: imei, version: version}} //参数
const url = 'xxx.xxx.xxx'; // 接口地址
this.http.get(url).subscribe(res => {
// 判断版本号
if (res && ((res as any).status > 0) && ((res as any).data.version !== version)) {
let apkUrl = (res as any).data.path; // apk下载路径
if ((res as any).data.force_update) { //是否强制升级(有些版本更迭是强制的,所以用户必须安装)
this.alertCtrl.create({
title: '升级提示',
subTitle: '发现新版本,是否立即升级?',
enableBackdropDismiss: false,
buttons: [{
text: '确定',
handler: () => {
this.storagePermissions().then(res => {
if (res) {
this.downloadApp(apkUrl);
}
})
}
}]
}).present();
} else {
this.alertCtrl.create({
title: '升级提示',
subTitle: '发现新版本,是否立即升级?',
enableBackdropDismiss: false,
buttons: [{
text: '取消'
}, {
text: '确定',
handler: () => {
// this.downloadApp(apkUrl);
this.storagePermissions().then(res => {
if (res) {
this.downloadApp(apkUrl);
}
})
}
}]
}).present();
}
}
}, error => {
})
})
})
}
/**
* 下载安装app
*/
downloadApp(url: any) {
let options;
options = {
title: '下载进度',
subTitle: '当前已下载: 0%',
enableBackdropDismiss: false
}
let alert = this.alertCtrl.create(options);
alert.present();
const fileTransfer: FileTransferObject = this.transfer.create();
console.log(this.file.externalRootDirectory)
const apk = this.file.externalRootDirectory + 'android.apk'; //apk保存的目录
fileTransfer.download(url, apk).then(() => {
this.fileOpener.open(apk, 'application/vnd.android.package-archive').then(() => {
}).catch(e => {
console.log('Error opening file' + e)
});
}).catch(err => {
// 存储权限出问题
this.toastCtrl.create({
message: '存储apk失败,请检查您是否关闭了存储权限!',
duration: 3000,
position: 'bottom'
}).present();
});
fileTransfer.onProgress((event: ProgressEvent) => {
let num = Math.floor(event.loaded / event.total * 100);
let title = document.getElementsByClassName('alert-sub-title')[0];
if (num === 100) {
// alert.dismiss();
title && (title.innerHTML = '下载完成,请您完成安装');
} else {
title && (title.innerHTML = '当前已下载:' + num + '%');
}
});
}
/**
* 获得app版本号,如0.01
* @description 对应/config.xml中version的值
* @returns {Promise}
*/
getVersionNumber(): Promise {
return new Promise((resolve) => {
this.appVersion.getVersionNumber().then((value: string) => {
resolve(value);
}).catch(err => {
console.log('getVersionNumber:' + err);
});
});
}
/**
* 获取imei号
*/
async getImei() {
const {hasPermission} = await this.androidPermissions.checkPermission(
this.androidPermissions.PERMISSION.READ_PHONE_STATE
);
if (!hasPermission) {
const result = await this.androidPermissions.requestPermission(
this.androidPermissions.PERMISSION.READ_PHONE_STATE
);
if (!result.hasPermission) {
// throw new Error('Permissions required');
this.platform.exitApp(); // 因为必须,所以被拒绝就退出app
}
return;
}
return this.uid.IMEI
}
/**
* 存储运行时权限
* apk下载时请求存储权限
*
*/
async storagePermissions() {
const {hasPermission} = await this.androidPermissions.checkPermission(
this.androidPermissions.PERMISSION.READ_EXTERNAL_STORAGE
);
if (!hasPermission) {
const result = await this.androidPermissions.requestPermission(
this.androidPermissions.PERMISSION.READ_EXTERNAL_STORAGE
);
if (!result.hasPermission) {
// throw new Error('存储权限被拒绝');
this.platform.exitApp(); // 因为必须,所以被拒绝就退出app
}
return true;
}
return true;
}
}
使用
在app.module.ts中注入需要的服务
import {File} from "@ionic-native/file";
import {FileTransfer, FileTransferObject} from '@ionic-native/file-transfer';
import {AppVersion} from '@ionic-native/app-version';
import {AndroidPermissions} from '@ionic-native/android-permissions/';
import {Uid} from '@ionic-native/uid';
import {NativeService} from './NativeService'
import {FileOpener} from "@ionic-native/file-opener";
providers: [
FileTransfer,
File,
NativeService,
AppVersion,
Uid,
AndroidPermissions,
FileOpener,
FileTransferObject,
]
使用
在app.component.ts使用
constructor(private nativeService: NativeService,...) {
platform.ready().then(() => {
...
this.nativeService.detectionUpgrade();
...
});
}
注意问题
- android升级后通过fileOpener打开apk不出现完成打开按钮
原因:
fileOpener2插件问题
处理方法:
找到platforms下的Android源码,找到fileOpener的Java类,添加如下代码:
一般该类目录为:io.github.pwlin.cordova.plugins.fileopener2;
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
private void _open(String fileArg, String contentType, Boolean openWithDefault, CallbackContext callbackContext) throws JSONException {
String fileName = "";
try {
CordovaResourceApi resourceApi = webView.getResourceApi();
Uri fileUri = resourceApi.remapUri(Uri.parse(fileArg));
fileName = this.stripFileProtocol(fileUri.toString());
} catch (Exception e) {
fileName = fileArg;
}
File file = new File(fileName);
if (file.exists()) {
try {
Uri path = Uri.fromFile(file);
Intent intent = new Intent(Intent.ACTION_VIEW);
if ((Build.VERSION.SDK_INT >= 23 && !contentType.equals("application/vnd.android.package-archive")) || ((Build.VERSION.SDK_INT == 24 || Build.VERSION.SDK_INT == 25) && contentType.equals("application/vnd.android.package-archive"))) {
Context context = cordova.getActivity().getApplicationContext();
path = FileProvider.getUriForFile(context, cordova.getActivity().getPackageName() + ".opener.provider", file);
intent.setDataAndType(path, contentType);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//这里
//intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
List infoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : infoList) {
String packageName = resolveInfo.activityInfo.packageName;
context.grantUriPermission(packageName, path, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
} else {
intent.setDataAndType(path, contentType);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//这里
}
/*
* @see
* http://stackoverflow.com/questions/14321376/open-an-activity-from-a-cordovaplugin
*/
if (openWithDefault) {
cordova.getActivity().startActivity(intent);
} else {
cordova.getActivity().startActivity(Intent.createChooser(intent, "Open File in..."));
}
callbackContext.success();
} catch (android.content.ActivityNotFoundException e) {
JSONObject errorObj = new JSONObject();
errorObj.put("status", PluginResult.Status.ERROR.ordinal());
errorObj.put("message", "Activity not found: " + e.getMessage());
callbackContext.error(errorObj);
}
} else {
JSONObject errorObj = new JSONObject();
errorObj.put("status", PluginResult.Status.ERROR.ordinal());
errorObj.put("message", "File not found");
callbackContext.error(errorObj);
}
}
这样修改如果每次重新生成平台都得改,也可以直接修改插件里面.在安装插件时就已经修改.
- android8无法自动打开安装程序(权限拒绝)
因为android8的权限问题,apk下载完成后无法正常自动打开安装程序,所以必须将平台targetSdkVersion版本进行修改.
修改latformsandroidappsrcmainAndroidManifest.xml里面targetSdkVersion的值为23.(所以得先添加平台,修改后再编译)
- error: resource android:attr/fontVariationSettings resource android:attr/ttcIndex not found.
方案一: 在/platforms/android/build.gradle和/platforms/android/app/build.gradle中添加如下代码.
configurations.all {
resolutionStrategy {
force 'com.android.support:support-v4:27.1.0'
}
}
方案二: 下载(推荐)
安装cordova-android-support-gradle-release插件
ionic cordova plugin add cordova-android-support-gradle-release --fetch