升级是应用最基本的功能,因为很少有一个应用发布后不在进行后期维护!
原生应用的升级比较常见,但是如今混合应用大热,因为项目,我就基于ionic框架实现了一个简单的升级,根据服务器端返回来确定强制还是非强制更新.

插件安装

file(访问文件)

1
2
ionic cordova plugin add cordova-plugin-file
npm install --save @ionic-native/file

File Transfer(上载和下载文件)

1
2
ionic cordova plugin add cordova-plugin-file-transfer
npm install --save @ionic-native/file-transfer

App Version(用来获取版本号)

1
2
ionic cordova plugin add cordova-plugin-app-version
npm install --save @ionic-native/app-version

Uid(获取设备标识,主要用于灰度升级,只升级用不到)

1
2
ionic cordova plugin add cordova-plugin-uid
npm install --save @ionic-native/uid

File Opener(打开下载完成的apk文件)

1
2
ionic cordova plugin add cordova-plugin-file-opener2
npm install --save @ionic-native/file-opener

Android Permissions(获取运行时权限)

1
2
ionic cordova plugin add cordova-plugin-android-permissions
npm install --save @ionic-native/android-permissions

升级服务

在src/app文件夹下创建NativeService.ts升级服务.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
/**
* 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<string>}
*/
getVersionNumber(): Promise<string> {
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中注入需要的服务

1
2
3
4
5
6
7
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";
1
2
3
4
5
6
7
8
9
10
11
providers: [
FileTransfer,
File,
NativeService,
AppVersion,
Uid,
AndroidPermissions,
FileOpener,
FileTransferObject,
]

使用

在app.component.ts使用

1
2
3
4
5
6
7
constructor(private nativeService: NativeService,...) {
platform.ready().then(() => {
...
this.nativeService.detectionUpgrade();
...
});
}

注意问题

  1. android升级后通过fileOpener打开apk不出现完成打开按钮

原因: fileOpener2插件问题

处理方法:

找到platforms下的Android源码,找到fileOpener的Java类,添加如下代码:

一般该类目录为:io.github.pwlin.cordova.plugins.fileopener2;

1
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

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<ResolveInfo> 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);
}
}

这样修改如果每次重新生成平台都得改,也可以直接修改插件里面.在安装插件时就已经修改.

  1. android8无法自动打开安装程序(权限拒绝)

因为android8的权限问题,apk下载完成后无法正常自动打开安装程序,所以必须将平台targetSdkVersion版本进行修改.

修改latforms\android\app\src\main\AndroidManifest.xml里面targetSdkVersion的值为23.(所以得先添加平台,修改后再编译)

  1. error: resource android:attr/fontVariationSettings resource android:attr/ttcIndex not found.

方案一: 在/platforms/android/build.gradle和/platforms/android/app/build.gradle中添加如下代码.

1
2
3
4
5
configurations.all {
resolutionStrategy {
force 'com.android.support:support-v4:27.1.0'
}
}

方案二: 下载(推荐)

安装cordova-android-support-gradle-release插件

1
ionic cordova plugin add cordova-android-support-gradle-release --fetch