Express.js + Android开发在线升级功能
Node代码
首先在服务器端开发代码,并运行Node代码
const Express = require('express');
const BodyParser =require('body-parser');
const Resp = {
success: (data, msg)=>{
return {
code: 1,
msg: msg || '获取成功',
data: data
}
},
error: (data, msg)=>{
return {
code: -1,
msg: msg || '获取失败',
data: data
}
},
}
const appExpress = Express();
appExpress.use(BodyParser.urlencoded({ extended: true, limit: "10mb" }));
appExpress.use(BodyParser.json({ extended: true, limit: "10mb" }));
appExpress.use(BodyParser.json());
// app 放在 此文件的同级目录 /APK中,把APK文件夹设置为静态文件夹
appExpress.use('/APK', Express.static(__dirname + '/APK'));
const VERSION = "1.0.3"
appExpress.get('/test',(req,res)=>{
res.send(Resp.success({ },'测试成功'));
});
// 获取版本号
appExpress.get('/appVersion',(req,res)=>{
res.send(Resp.success(VERSION,'获取成功'));
});
/*启动服务*/
appExpress.listen(9000,()=>{
GlobalLogListening({
title: "服务启动成功",
message: "The server is started successfully:9000--(服务启动成功)",
type: "successful"
})
})
Android APP在线升级功能
1、APP升级功能全部封装为一个Class
package com.hxtx.august.Server;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.core.content.FileProvider;
import com.hxtx.august.Action.FtpManageAction;
import com.hxtx.august.R;
import com.hxtx.august.tools.MessageDialog;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
public class AutoUpdater {
// 下载安装包的网络路径
private final String COMMON_URL = "http://192.168.3.165:9000/";
private String apkName = "app-release_1.0.2.apk";
private final String upAppUrl = COMMON_URL + "appVersion";
private String apkUrl = COMMON_URL + "APK/"+apkName;
// 当前进度
private int progress;
// 应用程序Context
private final Context mContext;
private boolean intercept = false;
// 进度条与通知UI刷新的handler和msg常量
private ProgressBar mProgress;
private TextView txtStatus;
private AlertDialog downloadDialog;
private static final int DOWN_UPDATE = 1;
private static final int DOWN_OVER = 2;
private static final int SHOWDOWN = 3;
public AutoUpdater(Context context) {
mContext = context;
}
/**
* 检查是否更新的内容
*/
public void CheckUpdate() {
new Thread(new Runnable() {
@Override
public void run() {
try {
PackageManager manager = mContext.getPackageManager();
// 获取本地APP版本
PackageInfo info = manager.getPackageInfo(mContext.getPackageName(), 0);
String appVersionName = info.versionName;
String result = doGet(upAppUrl);
// 处理返回的数据
if (result != null && result.length() > 0) {
JSONObject resultObject = new JSONObject(result);
Integer code = resultObject.getInt("code");
String msg = resultObject.getString("msg");
String version = resultObject.getString("data");
if (code == 1) {
int comResult = compareVersion(version,appVersionName);
if(comResult == 1){
apkName = "app-release_"+version+".apk";
apkUrl = COMMON_URL + "APK/" + apkName;
mHandler.sendEmptyMessage(SHOWDOWN);
}
} else {
MessageDialog.showMsg((Activity) mContext,"APP版本获取失败:"+msg);
}
} else {
MessageDialog.showMsg((Activity) mContext,"APP版本获取失败: 服务器数据空");
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
MessageDialog.showMsg((Activity) mContext,"APP版本获取失败: "+ e.getMessage());
} catch (JSONException e) {
e.printStackTrace();
MessageDialog.showMsg((Activity) mContext,"数据解析错误: "+ e.getMessage());
}
}
}).start();
}
public void ShowUpdateDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setTitle("软件版本更新");
builder.setMessage("有最新的软件包,请下载并安装!");
builder.setPositiveButton("立即下载", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ShowDownloadDialog();
}
});
builder.setNegativeButton("以后再说", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
builder.create().show();
}
public void ShowDownloadDialog() {
AlertDialog.Builder dialog = new AlertDialog.Builder(mContext);
dialog.setTitle("软件版本更新");
LayoutInflater inflater = LayoutInflater.from(mContext);
View v = inflater.inflate(R.layout.dialog_app_up_progress, null);
mProgress = (ProgressBar) v.findViewById(R.id.progress);
txtStatus = v.findViewById(R.id.txtStatus);
dialog.setView(v);
dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
intercept = true;
}
});
downloadDialog = dialog.show();
DownloadApk();
}
/**
如果返回 1,则表示 version1(本地版本)比 version2(最新版本)旧,因此需要进行更新。
如果返回 0,则表示 version1 和 version2 是相同的版本,无需更新。
如果返回 -1,则表示 version1(本地版本)比 version2(最新版本)新,这通常不应该发生,除非存在某种错误或特殊情况。
*/
public static int compareVersion(String oldVersion, String newVersion) {
String[] parts1 = oldVersion.split("\\.");
String[] parts2 = newVersion.split("\\.");
int length = Math.max(parts1.length, parts2.length);
for (int i = 0; i < length; i++) {
int part1 = i < parts1.length ? Integer.parseInt(parts1[i]) : 0;
int part2 = i < parts2.length ? Integer.parseInt(parts2[i]) : 0;
if (part1 < part2) {
return -1; // version1 is older
} else if (part1 > part2) {
return 1; // version1 is newer
}
}
return 0; // versions are equal
}
/**
* 从服务器下载APK安装包
*/
public void DownloadApk() {
// 下载线程
new Thread(new Runnable() {
@Override
public void run() {
URL url;
try {
File androidAppUrl = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
File localFolder = new File(androidAppUrl.getPath(),"AugustData");
boolean b = !localFolder.exists() && localFolder.mkdirs();
File localFile = new File(localFolder,apkName);
if(localFile.exists()) {
localFile.delete();
}
url = new URL(apkUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//必须加上这一句获取的文件才会有大小(原来获取的数据是采用gzip压缩的格式的,所以读不出来数据。下面这句可以解决)
conn.setRequestProperty("Accept-Encoding", "identity");
conn.connect();
int length = conn.getContentLength();byte[] buf = new byte[1024];
InputStream ins = conn.getInputStream();
FileOutputStream fos = new FileOutputStream(localFile);
int count = 0;
while (!intercept) {
int numread = ins.read(buf);
count += numread;
progress = (int) (((float) count / length) * 100);
// 下载进度
mHandler.sendEmptyMessage(DOWN_UPDATE);
if (numread <= 0) {
// 下载完成通知安装
Message msg = Message.obtain();
msg.what = DOWN_OVER;msg.obj = localFile;
mHandler.sendMessage(msg);
break;
}
fos.write(buf, 0, numread);
}
fos.close();
ins.close();
} catch (Exception e) {
e.printStackTrace();
MessageDialog.showMsg((Activity) mContext,"下载APK失败: "+ e.getMessage());
}
}
}).start();
}
/**
* 安装APK内容
*/
public void installAPK(File filePath) {
try {
if (!filePath.exists()) {
return;
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//安装完成后打开新版本
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 给目标应用一个临时授权
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {//判断版本大于等于7.0
//如果SDK版本>=24,即:Build.VERSION.SDK_INT >= 24,使用FileProvider兼容安装apk
String packageName = mContext.getApplicationContext().getPackageName();
String authority = new StringBuilder(packageName).append(".fileprovider").toString();
Uri apkUri = FileProvider.getUriForFile(mContext, authority, filePath);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(filePath), "application/vnd.android.package-archive");
}
mContext.startActivity(intent);
//android.os.Process.killProcess(android.os.Process.myPid());
// 安装完之后会提示”完成” “打开”,在android11之后这里不可以执行这一句,会提示解析错误。
} catch (Exception e) {
MessageDialog.showMsg((Activity) mContext,"安装APK失败: "+ e.getMessage());
}
}
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case SHOWDOWN:
ShowUpdateDialog();
break;
case DOWN_UPDATE:
txtStatus.setText("正在下载中:" + progress + "%");
mProgress.setProgress(progress);
break;
case DOWN_OVER:
File filePath = (File) msg.obj;
downloadDialog.dismiss();
installAPK(filePath);
break;
default:
break;
}
}
};
public static String doGet(String httpurl) {
HttpURLConnection connection = null;
InputStream is = null;
BufferedReader br = null;
String result = null;
try {
URL url = new URL(httpurl);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(15000);
connection.setReadTimeout(60000);
connection.connect();
if (connection.getResponseCode() == 200) {
is = connection.getInputStream();
br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
StringBuffer sbf = new StringBuffer();
String temp = null;
while ((temp = br.readLine()) != null) {
sbf.append(temp);
//sbf.append("\r\n");
}
result = sbf.toString();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
connection.disconnect();
}
return result;
}
}
2、进度条界面
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:orientation="vertical"
tools:ignore="RtlSymmetry">
<ProgressBar
android:id="@+id/progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/txtStatus" />
<TextView
android:id="@+id/txtStatus"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="状态"
android:textSize="12sp"
android:textStyle="normal" />
</LinearLayout>
3、AndroidManifest.xml 配置
application 添加 android:networkSecurityConfig="@xml/network_security_config" 不然不能访问网络,关系访问接口和下载APK功能
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.August"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="31"
>
network-security-config.xml文件代码
在res/xml
中创建network-security-config.xml
文件
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<!-- Trust preinstalled CAs -->
<certificates src="system" />
<!-- Additionally trust user added CAs -->
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
添加provider:防止安装APK时操作文件夹没有权限
在AndroidManifest.xml文件中application中,和activity同级
我是androidx,所以使用的是androidx.core.content.FileProvider
<activity
android:name=".TestWifi"
android:exported="true" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true"
>
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
在res/xml
中创建file_paths.xml
文件
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--安装包文件存储路径-->
<external-files-path
name="my_download"
path="Download" />
<external-path
name="."
path="." />
</paths>