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

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

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
虎头金猫2 天前
4K 视频总卡在公网带宽?用 N1 + OpenList 把网盘播放链路重新理顺
运维·服务器·网络·python·容器·beautifulsoup·pandas
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
-梅3 天前
linux(8) 软硬链接
linux·运维·服务器
Wang's Blog3 天前
Java 项目实战: 外卖平台-文件下载与ServletOutputStream回写浏览器
服务器·项目开发
琥珀色糖3 天前
SimlpeHttp
linux·服务器