QGC App 端远程升级接入实战:从接口检查到弹窗安装
摘要
前几篇已经把服务端、部署、后台和 APK 自动识别都写完了。这篇回到 App 端,讲 QGC/地面站如何接入远程升级服务。
客户端链路可以概括为:
text
配置升级服务器地址 -> 请求 /fra/check/update -> 解析 code=2000 -> 弹窗 -> 下载 APK -> 调起 Android 安装
适用场景
- QGC 二次开发项目需要 App 在线升级。
- 已经有 Node 升级服务,准备接入 Android 客户端。
- 需要从局域网测试切换到公网 ECS。
- 需要排查接口返回
code=2201、下载失败、安装失败等问题。
本文效果
完成后 App 可以:
- 启动或手动触发升级检查。
- 根据服务器返回判断是否有新版本。
- 显示更新说明。
- 下载 APK。
- 下载完成后调起 Android 安装界面。
背景
当前 App 端升级链路一般由三部分组成:
text
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 升级链路图
#mermaid-svg-C5wgbUoILAbCkFrb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-C5wgbUoILAbCkFrb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-C5wgbUoILAbCkFrb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-C5wgbUoILAbCkFrb .error-icon{fill:#552222;}#mermaid-svg-C5wgbUoILAbCkFrb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-C5wgbUoILAbCkFrb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-C5wgbUoILAbCkFrb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-C5wgbUoILAbCkFrb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-C5wgbUoILAbCkFrb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-C5wgbUoILAbCkFrb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-C5wgbUoILAbCkFrb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-C5wgbUoILAbCkFrb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-C5wgbUoILAbCkFrb .marker.cross{stroke:#333333;}#mermaid-svg-C5wgbUoILAbCkFrb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-C5wgbUoILAbCkFrb p{margin:0;}#mermaid-svg-C5wgbUoILAbCkFrb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-C5wgbUoILAbCkFrb .cluster-label text{fill:#333;}#mermaid-svg-C5wgbUoILAbCkFrb .cluster-label span{color:#333;}#mermaid-svg-C5wgbUoILAbCkFrb .cluster-label span p{background-color:transparent;}#mermaid-svg-C5wgbUoILAbCkFrb .label text,#mermaid-svg-C5wgbUoILAbCkFrb span{fill:#333;color:#333;}#mermaid-svg-C5wgbUoILAbCkFrb .node rect,#mermaid-svg-C5wgbUoILAbCkFrb .node circle,#mermaid-svg-C5wgbUoILAbCkFrb .node ellipse,#mermaid-svg-C5wgbUoILAbCkFrb .node polygon,#mermaid-svg-C5wgbUoILAbCkFrb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-C5wgbUoILAbCkFrb .rough-node .label text,#mermaid-svg-C5wgbUoILAbCkFrb .node .label text,#mermaid-svg-C5wgbUoILAbCkFrb .image-shape .label,#mermaid-svg-C5wgbUoILAbCkFrb .icon-shape .label{text-anchor:middle;}#mermaid-svg-C5wgbUoILAbCkFrb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-C5wgbUoILAbCkFrb .rough-node .label,#mermaid-svg-C5wgbUoILAbCkFrb .node .label,#mermaid-svg-C5wgbUoILAbCkFrb .image-shape .label,#mermaid-svg-C5wgbUoILAbCkFrb .icon-shape .label{text-align:center;}#mermaid-svg-C5wgbUoILAbCkFrb .node.clickable{cursor:pointer;}#mermaid-svg-C5wgbUoILAbCkFrb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-C5wgbUoILAbCkFrb .arrowheadPath{fill:#333333;}#mermaid-svg-C5wgbUoILAbCkFrb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-C5wgbUoILAbCkFrb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-C5wgbUoILAbCkFrb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C5wgbUoILAbCkFrb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-C5wgbUoILAbCkFrb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C5wgbUoILAbCkFrb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-C5wgbUoILAbCkFrb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-C5wgbUoILAbCkFrb .cluster text{fill:#333;}#mermaid-svg-C5wgbUoILAbCkFrb .cluster span{color:#333;}#mermaid-svg-C5wgbUoILAbCkFrb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-C5wgbUoILAbCkFrb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-C5wgbUoILAbCkFrb rect.text{fill:none;stroke-width:0;}#mermaid-svg-C5wgbUoILAbCkFrb .icon-shape,#mermaid-svg-C5wgbUoILAbCkFrb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C5wgbUoILAbCkFrb .icon-shape p,#mermaid-svg-C5wgbUoILAbCkFrb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-C5wgbUoILAbCkFrb .icon-shape .label rect,#mermaid-svg-C5wgbUoILAbCkFrb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C5wgbUoILAbCkFrb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-C5wgbUoILAbCkFrb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-C5wgbUoILAbCkFrb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 2000
2201
App 启动或用户点击检查更新
读取当前 versionCode
APPUpgradeManager 请求服务器
POST /fra/check/update
服务器返回 code
保存新版本信息
QML 弹出升级窗口
下载 APK
下载完成
AndroidCall.instalAPK
QGCActivity.installApk
系统安装界面
不提示更新
1. 配置服务器地址
App 端最小改动是把升级服务器地址配置好。
示例:
cpp
#ifdef STGC_RELEASE_ENABLED
#define STGC_HTTP_ADDRESS "https://upgrade.example.com/fra/"
#endif
测试阶段也可以用公网 IP:
cpp
#define STGC_HTTP_ADDRESS "http://203.0.113.10:8080/fra/"
注意结尾必须保留 /fra/,因为客户端会拼接:
cpp
QString url = QString(STGC_HTTP_ADDRESS) + "check/update";
最终请求地址是:
text
http://203.0.113.10:8080/fra/check/update
如果少了最后的 /fra/,很容易拼出错误地址。
2. 请求参数
服务端接收的是表单格式:
text
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 的 AndroidversionCode。app_flavor:渠道或构建类型。
服务端会先按 app_ids 匹配,再比较 versionCode。
3. 服务端返回格式
客户端当前强依赖的字段:
json
{
"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必须是手机能访问的公网地址。
服务端完整返回一般还会带:
json
{
"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": ""
}
}
无更新时:
json
{
"code": 2201,
"msg": "already latest"
}
4. Android 版本号规则
Android 当前版本在 AndroidManifest.xml 中:
xml
<manifest
package="com.example.gcs"
android:versionName="1.03.07"
android:versionCode="10307">
</manifest>
远程升级判断用的是 versionCode,不是 versionName。
所以新包必须让 versionCode 增大:
xml
<manifest
package="com.example.gcs"
android:versionName="1.03.08"
android:versionCode="10308">
</manifest>
如果只改了 versionName,但 versionCode 没变,服务端会认为已经是最新版本。
5. 客户端请求流程
伪代码大概是这样:
cpp
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 层负责弹窗:
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:
cpp
void APPUpgradeManager::installApk()
{
#ifdef Q_OS_ANDROID
AndroidCall::instalAPK(_localApkPath);
#endif
}
Android Java 侧调起安装界面:
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 配置
需要网络权限:
xml
<uses-permission android:name="android.permission.INTERNET" />
需要安装未知来源 APK 的权限:
xml
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
需要配置 FileProvider:
xml
<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
<?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. 本地联调
本地启动服务:
powershell
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 临时配置:
cpp
#define STGC_HTTP_ADDRESS "http://192.168.1.100:8080/fra/"
用 PowerShell 先模拟请求:
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. 公网联调
服务器验证:
text
http://203.0.113.10:8080/health
App 配置:
cpp
#define STGC_HTTP_ADDRESS "http://203.0.113.10:8080/fra/"
接口验证:
powershell
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'
完整验收顺序建议:
#mermaid-svg-pMz3IlZqvw8RNK82{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pMz3IlZqvw8RNK82 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pMz3IlZqvw8RNK82 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pMz3IlZqvw8RNK82 .error-icon{fill:#552222;}#mermaid-svg-pMz3IlZqvw8RNK82 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pMz3IlZqvw8RNK82 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pMz3IlZqvw8RNK82 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pMz3IlZqvw8RNK82 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pMz3IlZqvw8RNK82 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pMz3IlZqvw8RNK82 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pMz3IlZqvw8RNK82 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pMz3IlZqvw8RNK82 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pMz3IlZqvw8RNK82 .marker.cross{stroke:#333333;}#mermaid-svg-pMz3IlZqvw8RNK82 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pMz3IlZqvw8RNK82 p{margin:0;}#mermaid-svg-pMz3IlZqvw8RNK82 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pMz3IlZqvw8RNK82 .cluster-label text{fill:#333;}#mermaid-svg-pMz3IlZqvw8RNK82 .cluster-label span{color:#333;}#mermaid-svg-pMz3IlZqvw8RNK82 .cluster-label span p{background-color:transparent;}#mermaid-svg-pMz3IlZqvw8RNK82 .label text,#mermaid-svg-pMz3IlZqvw8RNK82 span{fill:#333;color:#333;}#mermaid-svg-pMz3IlZqvw8RNK82 .node rect,#mermaid-svg-pMz3IlZqvw8RNK82 .node circle,#mermaid-svg-pMz3IlZqvw8RNK82 .node ellipse,#mermaid-svg-pMz3IlZqvw8RNK82 .node polygon,#mermaid-svg-pMz3IlZqvw8RNK82 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pMz3IlZqvw8RNK82 .rough-node .label text,#mermaid-svg-pMz3IlZqvw8RNK82 .node .label text,#mermaid-svg-pMz3IlZqvw8RNK82 .image-shape .label,#mermaid-svg-pMz3IlZqvw8RNK82 .icon-shape .label{text-anchor:middle;}#mermaid-svg-pMz3IlZqvw8RNK82 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pMz3IlZqvw8RNK82 .rough-node .label,#mermaid-svg-pMz3IlZqvw8RNK82 .node .label,#mermaid-svg-pMz3IlZqvw8RNK82 .image-shape .label,#mermaid-svg-pMz3IlZqvw8RNK82 .icon-shape .label{text-align:center;}#mermaid-svg-pMz3IlZqvw8RNK82 .node.clickable{cursor:pointer;}#mermaid-svg-pMz3IlZqvw8RNK82 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pMz3IlZqvw8RNK82 .arrowheadPath{fill:#333333;}#mermaid-svg-pMz3IlZqvw8RNK82 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pMz3IlZqvw8RNK82 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pMz3IlZqvw8RNK82 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pMz3IlZqvw8RNK82 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pMz3IlZqvw8RNK82 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pMz3IlZqvw8RNK82 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pMz3IlZqvw8RNK82 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pMz3IlZqvw8RNK82 .cluster text{fill:#333;}#mermaid-svg-pMz3IlZqvw8RNK82 .cluster span{color:#333;}#mermaid-svg-pMz3IlZqvw8RNK82 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pMz3IlZqvw8RNK82 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pMz3IlZqvw8RNK82 rect.text{fill:none;stroke-width:0;}#mermaid-svg-pMz3IlZqvw8RNK82 .icon-shape,#mermaid-svg-pMz3IlZqvw8RNK82 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pMz3IlZqvw8RNK82 .icon-shape p,#mermaid-svg-pMz3IlZqvw8RNK82 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pMz3IlZqvw8RNK82 .icon-shape .label rect,#mermaid-svg-pMz3IlZqvw8RNK82 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pMz3IlZqvw8RNK82 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pMz3IlZqvw8RNK82 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pMz3IlZqvw8RNK82 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 服务器 /health
PowerShell 调 /fra/check/update
浏览器打开 fileUrl
App 真机检查更新
App 下载 APK
系统安装界面弹出
安装后 versionCode 变大
常见 code=2201 排查
code=2201 不一定是接口错了,它表示"没有可升级版本"。
重点检查:
1. app_ids 不匹配
客户端传:
text
com.example.gcs
服务端必须配置:
json
"appIds": ["com.example.gcs"]
2. app_code 已经是最新
客户端传:
text
app_code=10308
服务端最新也是:
json
"versionCode": 10308
这时返回 already latest 是正常的。
3. 后台版本未启用
检查:
json
"enabled": true
如果是 false,不参与升级判断。
4. 请求地址不对
正确:
text
http://203.0.113.10:8080/fra/check/update
错误示例:
text
http://203.0.113.10:8080/check/update
少了 /fra/。
常见下载安装问题
1. APK 下载失败
检查 fileUrl 是否能在手机浏览器打开。
如果返回的是:
text
http://localhost:8080/updates/xxx.apk
说明服务端 PUBLIC_BASE_URL 配错了。
2. 安装界面打不开
检查:
- 是否配置
REQUEST_INSTALL_PACKAGES。 - 是否使用
FileProvider。 authorities是否和getPackageName() + ".fileprovider"一致。- Android 设置里是否允许当前 App 安装未知应用。
3. 安装后还提示升级
一般是新 APK 的 versionCode 没有变大,或者服务端配置的版本号和 APK 实际版本号不一致。
小结
App 端接入远程升级,关键不在代码量,而在每个环节都要对齐:
text
STGC_HTTP_ADDRESS 以 /fra/ 结尾
app_ids 匹配 applicationId
app_code 使用 versionCode
fileUrl 必须公网可访问
Android 7+ 使用 FileProvider 安装
把这些点串起来后,QGC/地面站就可以从本地手动发包,升级到"服务器后台上传 APK,App 自动发现新版本并安装"的流程。
这个方案不复杂,但对项目交付很实用:测试包、客户现场包、多个地面站分支,都可以用同一套升级服务统一管理。