QGC App 端远程升级接入实战:从接口检查到弹窗安装

摘要

前几篇已经把服务端、部署、后台和 APK 自动识别都写完了。这篇回到 App 端,讲 QGC/地面站如何接入远程升级服务。

客户端链路可以概括为:

配置升级服务器地址 -> 请求 /fra/check/update -> 解析 code=2000 -> 弹窗 -> 下载 APK -> 调起 Android 安装

适用场景

  • QGC 二次开发项目需要 App 在线升级。
  • 已经有 Node 升级服务,准备接入 Android 客户端。
  • 需要从局域网测试切换到公网 ECS。
  • 需要排查接口返回 code=2201、下载失败、安装失败等问题。

本文效果

完成后 App 可以:

  • 启动或手动触发升级检查。
  • 根据服务器返回判断是否有新版本。
  • 显示更新说明。
  • 下载 APK。
  • 下载完成后调起 Android 安装界面。

背景

当前 App 端升级链路一般由三部分组成:

src/http/APPUpgradeManager.cpp
    请求服务器、解析更新信息、下载 APK

src/STGCControl/STGCUi/AppUpgradeView.qml
    发现更新后的弹窗、下载按钮、安装按钮

android/src/org/mavlink/qgroundcontrol/QGCActivity.java
    Android 侧读取版本号并调起 APK 安装界面

移植到其它地面站时,至少要把这些能力带过去:

  • C++ 升级管理器。
  • HTTP 请求工具。
  • QML 升级弹窗。
  • Android JNI 桥。
  • Java 安装 APK 逻辑。
  • AndroidManifest 权限和 FileProvider。

App 升级链路图

2000

2201

App 启动或用户点击检查更新

读取当前 versionCode

APPUpgradeManager 请求服务器

POST /fra/check/update

服务器返回 code

保存新版本信息

QML 弹出升级窗口

下载 APK

下载完成

AndroidCall.instalAPK

QGCActivity.installApk

系统安装界面

不提示更新

1. 配置服务器地址

App 端最小改动是把升级服务器地址配置好。

示例:

#ifdef STGC_RELEASE_ENABLED
#define STGC_HTTP_ADDRESS "https://upgrade.example.com/fra/"
#endif

测试阶段也可以用公网 IP:

#define STGC_HTTP_ADDRESS "http://203.0.113.10:8080/fra/"

注意结尾必须保留 /fra/,因为客户端会拼接:

QString url = QString(STGC_HTTP_ADDRESS) + "check/update";

最终请求地址是:

http://203.0.113.10:8080/fra/check/update

如果少了最后的 /fra/,很容易拼出错误地址。

2. 请求参数

服务端接收的是表单格式:

POST /fra/check/update
Content-Type: application/x-www-form-urlencoded

&app_ids=com.example.gcs&app_code=10307&app_flavor=release

字段说明:

  • app_ids:当前 App 的 applicationId。
  • app_code:当前 App 的 Android versionCode
  • app_flavor:渠道或构建类型。

服务端会先按 app_ids 匹配,再比较 versionCode

3. 服务端返回格式

客户端当前强依赖的字段:

{
  "code": 2000,
  "data": {
    "fileName": "GCS-Demo-1.03.08.apk",
    "fileUrl": "https://upgrade.example.com/updates/GCS-Demo-1.03.08.apk",
    "sizeKB": 211849,
    "description": "修复已知问题,优化升级流程",
    "descriptionEn": "Bug fixes and upgrade improvements"
  }
}

其中:

  • code == 2000 表示有新版本。
  • 其它 code 当前客户端可以按“无更新”处理。
  • fileUrl 必须是手机能访问的公网地址。

服务端完整返回一般还会带:

{
  "code": 2000,
  "msg": "new version available",
  "data": {
    "versionCode": 10308,
    "versionName": "1.03.08",
    "fileName": "GCS-Demo-1.03.08.apk",
    "fileUrl": "http://203.0.113.10:8080/updates/GCS-Demo-1.03.08.apk",
    "sizeKB": 211849,
    "force": false,
    "description": "修复已知问题,优化升级流程",
    "descriptionEn": ""
  }
}

无更新时:

{
  "code": 2201,
  "msg": "already latest"
}

4. Android 版本号规则

Android 当前版本在 AndroidManifest.xml 中:

<manifest
    package="com.example.gcs"
    android:versionName="1.03.07"
    android:versionCode="10307">
</manifest>

远程升级判断用的是 versionCode,不是 versionName

所以新包必须让 versionCode 增大:

<manifest
    package="com.example.gcs"
    android:versionName="1.03.08"
    android:versionCode="10308">
</manifest>

如果只改了 versionName,但 versionCode 没变,服务端会认为已经是最新版本。

5. 客户端请求流程

伪代码大概是这样:

void APPUpgradeManager::checkUpdate()
{
    QString url = QString(STGC_HTTP_ADDRESS) + "check/update";
    QByteArray body;

    body.append("&app_ids=");
    body.append(getApplicationId().toUtf8());
    body.append("&app_code=");
    body.append(QString::number(getVersionCode()).toUtf8());
    body.append("&app_flavor=release");

    postForm(url, body, [this](const QJsonObject& json) {
        if (json.value("code").toInt() != 2000) {
            return;
        }

        QJsonObject data = json.value("data").toObject();
        _fileName = data.value("fileName").toString();
        _fileUrl = data.value("fileUrl").toString();
        _description = data.value("description").toString();

        emit newVersionFound();
    });
}

实际项目里可以沿用已有的 HTTP 工具,只要保证:

  • 请求方法是 POST。
  • Content-Type 是 application/x-www-form-urlencoded
  • 参数名和服务端一致。
  • 成功后读取 data.fileUrl

6. QML 弹窗逻辑

App 发现新版本后,QML 层负责弹窗:

Popup {
    id: appUpgradeView
    modal: true

    Column {
        Text {
            text: qsTr("发现新版本")
        }

        Text {
            text: APPUpgradeManager.description
            wrapMode: Text.WordWrap
        }

        Button {
            text: qsTr("下载")
            onClicked: APPUpgradeManager.downloadApk()
        }

        Button {
            text: qsTr("安装")
            visible: APPUpgradeManager.downloadFinished
            onClicked: APPUpgradeManager.installApk()
        }
    }
}

实际项目可以按自己的 UI 风格调整,但状态最好明确:

  • 检查中。
  • 发现新版本。
  • 下载中。
  • 下载完成。
  • 下载失败。
  • 安装中。

7. 下载和安装 APK

下载完成后,C++ 层通常会调用 Android JNI:

void APPUpgradeManager::installApk()
{
#ifdef Q_OS_ANDROID
    AndroidCall::instalAPK(_localApkPath);
#endif
}

Android Java 侧调起安装界面:

public void installApk(String apkPath) {
    File apkFile = new File(apkPath);
    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

    Uri apkUri = FileProvider.getUriForFile(
        this,
        getPackageName() + ".fileprovider",
        apkFile
    );

    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
    startActivity(intent);
}

Android 7+ 以后不能直接用 file:// 暴露文件路径,必须使用 FileProvider

8. AndroidManifest 配置

需要网络权限:

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

需要安装未知来源 APK 的权限:

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

需要配置 FileProvider:

<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/provider_paths" />
</provider>

provider_paths.xml 示例:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-files-path name="downloads" path="." />
    <cache-path name="cache" path="." />
</paths>

9. 本地联调

本地启动服务:

cd D:\your_workspace\server
$env:ADMIN_PASSWORD="your-admin-password"
$env:PUBLIC_BASE_URL="http://192.168.1.100:8080"
npm start

注意这里不要写 localhost,手机访问不到电脑的 localhost

App 临时配置:

#define STGC_HTTP_ADDRESS "http://192.168.1.100:8080/fra/"

用 PowerShell 先模拟请求:

Invoke-RestMethod -Method Post `
  -Uri http://192.168.1.100:8080/fra/check/update `
  -ContentType 'application/x-www-form-urlencoded' `
  -Body '&app_ids=com.example.gcs&app_code=10307&app_flavor=release'

如果接口没问题,再上真机调试。

10. 公网联调

服务器验证:

http://203.0.113.10:8080/health

App 配置:

#define STGC_HTTP_ADDRESS "http://203.0.113.10:8080/fra/"

接口验证:

Invoke-RestMethod -Method Post `
  -Uri http://203.0.113.10:8080/fra/check/update `
  -ContentType 'application/x-www-form-urlencoded' `
  -Body '&app_ids=com.example.gcs&app_code=10307&app_flavor=release'

完整验收顺序建议:

服务器 /health

PowerShell 调 /fra/check/update

浏览器打开 fileUrl

App 真机检查更新

App 下载 APK

系统安装界面弹出

安装后 versionCode 变大

常见 code=2201 排查

code=2201 不一定是接口错了,它表示“没有可升级版本”。

重点检查:

1. app_ids 不匹配

客户端传:

com.example.gcs

服务端必须配置:

"appIds": ["com.example.gcs"]

2. app_code 已经是最新

客户端传:

app_code=10308

服务端最新也是:

"versionCode": 10308

这时返回 already latest 是正常的。

3. 后台版本未启用

检查:

"enabled": true

如果是 false,不参与升级判断。

4. 请求地址不对

正确:

http://203.0.113.10:8080/fra/check/update

错误示例:

http://203.0.113.10:8080/check/update

少了 /fra/

常见下载安装问题

1. APK 下载失败

检查 fileUrl 是否能在手机浏览器打开。

如果返回的是:

http://localhost:8080/updates/xxx.apk

说明服务端 PUBLIC_BASE_URL 配错了。

2. 安装界面打不开

检查:

  • 是否配置 REQUEST_INSTALL_PACKAGES
  • 是否使用 FileProvider
  • authorities 是否和 getPackageName() + ".fileprovider" 一致。
  • Android 设置里是否允许当前 App 安装未知应用。

3. 安装后还提示升级

一般是新 APK 的 versionCode 没有变大,或者服务端配置的版本号和 APK 实际版本号不一致。

小结

App 端接入远程升级,关键不在代码量,而在每个环节都要对齐:

STGC_HTTP_ADDRESS 以 /fra/ 结尾
app_ids 匹配 applicationId
app_code 使用 versionCode
fileUrl 必须公网可访问
Android 7+ 使用 FileProvider 安装

把这些点串起来后,QGC/地面站就可以从本地手动发包,升级到“服务器后台上传 APK,App 自动发现新版本并安装”的流程。

这个方案不复杂,但对项目交付很实用:测试包、客户现场包、多个地面站分支,都可以用同一套升级服务统一管理。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐