Express.js + Android开发在线升级功能

52 阅读5分钟

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>