Codex实战:APP远程升级服务搭建(五)App端远程升级接入

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 的 Android versionCode
  • 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 自动发现新版本并安装"的流程。

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

相关推荐
Web极客码1 小时前
使用FeedBurner优化WordPress订阅体验
服务器·wordpress·feedburner
Lang-12101 小时前
CentOS Linux服务器完整迁移方案
linux·服务器·centos
TCW11211 小时前
Linux操作系统系列.动态加载
linux·服务器
lisanmengmeng1 小时前
gitlab 免密配置
linux·服务器·gitlab
普马萨特1 小时前
Wi-Fi (802.11) 协议演进
运维·服务器·网络
BreezeDove1 小时前
【Android】Flutter3.35项目启动超时问题
android·flutter
袁小皮皮不皮1 小时前
2.HCIP OSPF路由基础(优化版)
运维·服务器·网络·网络协议·智能路由器
vsropy2 小时前
Ubuntu20 ping: www.baidu.com: 域名解析暂时失败的解决办法
运维·服务器
故渊at2 小时前
第十四板块:Android 硬件抽象与安全加固 | 第三十四篇:Hardware Composer (HWC) 与 显示安全(HDCP)
android·安全·composer·安全加固·hwc·硬件抽象