# Flutter Android APK 在线升级完整方案

> 支持 Google Play 和自建服务器双渠道的 Flutter Android 应用升级解决方案

## 目录

- [一、方案概述](#一方案概述)
- [二、技术栈与依赖](#二技术栈与依赖)
- [三、架构设计](#三架构设计)
- [四、Google Play 应用内更新](#四google-play-应用内更新)
- [五、自建服务器 APK 升级](#五自建服务器-apk-升级)
- [六、统一升级服务](#六统一升级服务)
- [七、后端接口设计](#七后端接口设计)
- [八、Android 原生配置](#八android-原生配置)
- [九、安全性考虑](#九安全性考虑)
- [十、实施步骤](#十实施步骤)

---

## 一、方案概述

### 1.1 升级渠道对比

| 渠道 | 适用场景 | 升级体验 | 审核要求 |
|------|---------|---------|---------|
| Google Play | 海外市场、正规渠道 | 应用内更新，体验流畅 | 需要审核 |
| 自建服务器 | 国内市场、企业内部 | 下载 APK 安装 | 无需审核 |

### 1.2 核心特性

- 支持 Google Play 应用内更新（灵活更新、强制更新）
- 支持自建服务器 APK 下载安装
- 使用应用专属目录，无需外部存储权限
- SHA-256 文件完整性校验
- 支持应用市场来源检测
- 渠道构建层面完全隔离
- 强制 HTTPS 下载

---

## 二、技术栈与依赖

### 2.1 Flutter 依赖

```yaml
dependencies:
  # 版本信息
  package_info_plus: ^8.0.0
  
  # HTTP 请求与下载
  dio: ^5.4.0
  
  # Google Play 应用内更新
  in_app_update: ^4.2.2
  
  # 权限管理
  permission_handler: ^11.0.0
  
  # 路径获取
  path_provider: ^2.1.0
  
  # SHA-256 校验
  crypto: ^3.0.3
  
  # URL 启动
  url_launcher: ^6.2.0
```

### 2.2 Android 依赖

Google Play Core Library 会由 `in_app_update` 插件自动添加。

---

## 三、架构设计

### 3.1 目录结构

```
lib/
└── core/
    └── updater/
        ├── models/
        │   ├── version_info.dart              # 版本信息模型
        │   └── update_config.dart             # 更新配置
        ├── channels/
        │   ├── google_play_updater.dart       # Google Play 更新
        │   └── self_hosted_updater.dart       # 自建服务器更新
        ├── version_checker.dart               # 版本检测
        ├── download_manager.dart              # 下载管理
        ├── apk_installer.dart                 # APK 安装器
        ├── app_market_detector.dart           # 应用市场检测
        └── update_service.dart                # 升级服务统一入口
```

---

## 四、Google Play 应用内更新

### 4.1 Google Play 更新器实现

**文件: `lib/core/updater/channels/google_play_updater.dart`**

```dart
import 'package:in_app_update/in_app_update.dart';

enum GooglePlayUpdateType {
  flexible,    // 灵活更新：后台下载，可继续使用
  immediate,   // 强制更新：阻塞式，必须更新
}

class GooglePlayUpdater {
  /// 检查是否有更新可用
  static Future<bool> checkForUpdate() async {
    try {
      final updateInfo = await InAppUpdate.checkForUpdate();
      return updateInfo.updateAvailability == UpdateAvailability.updateAvailable;
    } catch (e) {
      print('Check update failed: $e');
      return false;
    }
  }

  /// 灵活更新
  /// 用户可以在后台下载，继续使用应用
  static Future<void> performFlexibleUpdate() async {
    try {
      final updateInfo = await InAppUpdate.checkForUpdate();
      
      if (updateInfo.updateAvailability == UpdateAvailability.updateAvailable) {
        if (updateInfo.flexibleUpdateAllowed) {
          await InAppUpdate.startFlexibleUpdate();
          
          InAppUpdate.completeFlexibleUpdate().then((_) {
            print('Update completed, app will restart');
          }).catchError((e) {
            print('Update failed: $e');
          });
        } else {
          print('Flexible update not allowed');
        }
      }
    } catch (e) {
      print('Flexible update error: $e');
    }
  }

  /// 强制更新
  /// 阻塞式更新，用户必须更新才能继续使用
  static Future<void> performImmediateUpdate() async {
    try {
      final updateInfo = await InAppUpdate.checkForUpdate();
      
      if (updateInfo.updateAvailability == UpdateAvailability.updateAvailable) {
        if (updateInfo.immediateUpdateAllowed) {
          await InAppUpdate.performImmediateUpdate();
        } else {
          print('Immediate update not allowed');
        }
      }
    } catch (e) {
      print('Immediate update error: $e');
    }
  }

  /// 智能更新策略
  /// 根据版本差异决定更新方式
  static Future<void> smartUpdate({
    required int currentVersion,
    required int latestVersion,
  }) async {
    final versionDiff = latestVersion - currentVersion;
    
    if (versionDiff >= 10) {
      // 版本差异大，强制更新
      await performImmediateUpdate();
    } else {
      // 版本差异小，灵活更新
      await performFlexibleUpdate();
    }
  }
}
```

### 4.2 使用示例

```dart
class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    _checkGooglePlayUpdate();
  }

  Future<void> _checkGooglePlayUpdate() async {
    await Future.delayed(const Duration(seconds: 3));
    
    final hasUpdate = await GooglePlayUpdater.checkForUpdate();
    if (hasUpdate) {
      _showUpdateDialog();
    }
  }

  void _showUpdateDialog() {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => AlertDialog(
        title: const Text('发现新版本'),
        content: const Text('有新版本可用，是否立即更新？'),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              GooglePlayUpdater.performFlexibleUpdate();
            },
            child: const Text('更新'),
          ),
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('稍后'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(/* ... */);
  }
}
```

---

## 五、自建服务器 APK 升级

### 5.1 版本信息模型

**文件: `lib/core/updater/models/version_info.dart`**

```dart
import 'package:json_annotation/json_annotation.dart';

part 'version_info.g.dart';

@JsonSerializable()
class VersionInfo {
  final String version;          // 版本号: "1.2.0"
  final int versionCode;         // 版本代码: 120
  final String downloadUrl;      // APK 下载地址
  final int fileSize;            // 文件大小(字节)
  final String fileSizeFriendly; // 友好显示: "25.6 MB"
  final String sha256;           // SHA-256 校验
  final bool forceUpdate;        // 是否强制更新
  final String? updateLog;       // 更新日志
  final DateTime releaseDate;    // 发布时间

  VersionInfo({
    required this.version,
    required this.versionCode,
    required this.downloadUrl,
    required this.fileSize,
    required this.fileSizeFriendly,
    required this.sha256,
    this.forceUpdate = false,
    this.updateLog,
    required this.releaseDate,
  });

  factory VersionInfo.fromJson(Map<String, dynamic> json) =>
      _$VersionInfoFromJson(json);
  
  Map<String, dynamic> toJson() => _$VersionInfoToJson(this);
}
```

### 5.2 版本检测器

**文件: `lib/core/updater/version_checker.dart`**

```dart
import 'package:package_info_plus/package_info_plus.dart';
import 'package:dio/dio.dart';
import 'models/version_info.dart';

class VersionChecker {
  final String apiBaseUrl;
  final Dio _dio;

  VersionChecker({required this.apiBaseUrl})
      : _dio = Dio(BaseOptions(
          baseUrl: apiBaseUrl,
          connectTimeout: const Duration(seconds: 10),
        ));

  /// 获取当前版本信息
  Future<PackageInfo> getCurrentVersion() async {
    return await PackageInfo.fromPlatform();
  }

  /// 从服务器获取最新版本信息
  Future<VersionInfo?> fetchLatestVersion() async {
    try {
      final response = await _dio.get('/api/app/version/check');
      
      if (response.statusCode == 200) {
        return VersionInfo.fromJson(response.data);
      }
      return null;
    } catch (e) {
      print('Fetch version failed: $e');
      return null;
    }
  }

  /// 检查是否有新版本
  Future<VersionInfo?> checkForUpdate() async {
    try {
      final currentInfo = await getCurrentVersion();
      final latestInfo = await fetchLatestVersion();

      if (latestInfo == null) return null;

      final currentCode = int.parse(currentInfo.buildNumber);
      if (latestInfo.versionCode > currentCode) {
        return latestInfo;
      }

      return null;
    } catch (e) {
      print('Check update failed: $e');
      return null;
    }
  }

  /// 是否需要强制更新
  Future<bool> needForceUpdate() async {
    final latestInfo = await checkForUpdate();
    return latestInfo?.forceUpdate ?? false;
  }
}
```

### 5.3 下载管理器

**文件: `lib/core/updater/download_manager.dart`**

```dart
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:crypto/crypto.dart';
import 'dart:convert';

class DownloadManager {
  final Dio _dio = Dio();
  CancelToken? _cancelToken;

  /// 下载 APK 文件
  /// [url] 下载地址（必须是 HTTPS）
  /// [sha256Expected] SHA-256 校验值
  /// [onProgress] 下载进度回调 (已下载字节, 总字节)
  Future<File?> downloadApk({
    required String url,
    required String sha256Expected,
    Function(int received, int total)? onProgress,
  }) async {
    try {
      // 强制 HTTPS
      if (!url.startsWith('https://')) {
        print('Download URL must use HTTPS');
        return null;
      }

      _cancelToken = CancelToken();

      // 使用应用专属目录（无需额外权限）
      final dir = await getApplicationDocumentsDirectory();
      final savePath = '${dir.path}/app_update.apk';
      final file = File(savePath);

      if (await file.exists()) {
        await file.delete();
      }

      print('Downloading APK to: $savePath');

      await _dio.download(
        url,
        savePath,
        cancelToken: _cancelToken,
        onReceiveProgress: (received, total) {
          if (total != -1) {
            final progress = (received / total * 100).toStringAsFixed(0);
            print('Download progress: $progress%');
            onProgress?.call(received, total);
          }
        },
        options: Options(
          receiveTimeout: const Duration(minutes: 10),
          sendTimeout: const Duration(minutes: 10),
        ),
      );

      print('Download completed');

      // 校验 SHA-256
      final isValid = await _verifySha256(file, sha256Expected);
      if (!isValid) {
        print('SHA-256 verification failed');
        await file.delete();
        return null;
      }

      print('SHA-256 verified');
      return file;
    } catch (e) {
      print('Download failed: $e');
      return null;
    }
  }

  /// 取消下载
  void cancelDownload() {
    _cancelToken?.cancel('User cancelled download');
  }

  /// 校验文件 SHA-256
  Future<bool> _verifySha256(File file, String expectedSha256) async {
    try {
      final bytes = await file.readAsBytes();
      final digest = sha256.convert(bytes);
      final actualSha256 = digest.toString();
      
      print('Expected SHA-256: $expectedSha256');
      print('Actual SHA-256:   $actualSha256');
      
      return actualSha256.toLowerCase() == expectedSha256.toLowerCase();
    } catch (e) {
      print('SHA-256 verification error: $e');
      return false;
    }
  }
}
```

### 5.4 APK 安装器

**文件: `lib/core/updater/apk_installer.dart`**

```dart
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';

class ApkInstaller {
  static const MethodChannel _channel = MethodChannel('com.yourapp/apk_installer');

  /// 安装 APK
  static Future<bool> installApk(File apkFile) async {
    try {
      if (!await apkFile.exists()) {
        print('APK file not found');
        return false;
      }

      // Android 8.0+ 需要请求安装权限
      if (Platform.isAndroid) {
        final hasPermission = await _requestInstallPermission();
        if (!hasPermission) {
          print('Install permission denied');
          return false;
        }
      }

      print('Installing APK: ${apkFile.path}');
      final result = await _channel.invokeMethod('installApk', {
        'apkPath': apkFile.path,
      });
      
      print('Installation triggered: $result');
      return true;
    } catch (e) {
      print('Install failed: $e');
      return false;
    }
  }

  /// 请求安装权限（Android 8.0+）
  static Future<bool> _requestInstallPermission() async {
    if (await Permission.requestInstallPackages.isGranted) {
      return true;
    }

    final status = await Permission.requestInstallPackages.request();
    return status.isGranted;
  }
}
```

### 5.5 应用市场检测器

**文件: `lib/core/updater/app_market_detector.dart`**

```dart
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';

class AppMarketDetector {
  static const MethodChannel _channel = MethodChannel('com.yourapp/app_market');

  /// 获取安装来源
  static Future<String?> getInstallerPackageName() async {
    if (!Platform.isAndroid) return null;

    try {
      final installer = await _channel.invokeMethod<String>('getInstallerPackageName');
      return installer;
    } catch (e) {
      print('Get installer failed: $e');
      return null;
    }
  }

  /// 判断是否来自应用市场
  static Future<bool> isFromAppMarket() async {
    final installer = await getInstallerPackageName();
    
    if (installer == null || installer.isEmpty) {
      return false; // 直接安装
    }

    // 常见应用市场包名
    const marketPackages = [
      'com.huawei.appmarket',
      'com.xiaomi.market',
      'com.oppo.market',
      'com.bbk.appstore',
      'com.tencent.android.qqdownloader',
      'com.qihoo.appstore',
      'com.baidu.appsearch',
      'com.wandoujia.phoenix2',
      'com.dragon.android.pandaspace',
      'com.sec.android.app.samsungapps',
    ];

    return marketPackages.contains(installer);
  }

  /// 打开应用市场详情页
  static Future<void> openAppMarketDetail(String packageName) async {
    final marketUri = Uri.parse('market://details?id=$packageName');

    if (await canLaunchUrl(marketUri)) {
      await launchUrl(marketUri, mode: LaunchMode.externalApplication);
    } else {
      final webUri = Uri.parse('https://play.google.com/store/apps/details?id=$packageName');
      await launchUrl(webUri, mode: LaunchMode.externalApplication);
    }
  }
}
```

### 5.6 自建服务器更新器

**文件: `lib/core/updater/channels/self_hosted_updater.dart`**

```dart
import 'dart:io';
import 'package:flutter/material.dart';
import '../version_checker.dart';
import '../download_manager.dart';
import '../apk_installer.dart';
import '../app_market_detector.dart';
import '../models/version_info.dart';

class SelfHostedUpdater {
  final VersionChecker versionChecker;
  final DownloadManager downloadManager;

  SelfHostedUpdater({
    required String apiBaseUrl,
  })  : versionChecker = VersionChecker(apiBaseUrl: apiBaseUrl),
        downloadManager = DownloadManager();

  /// 检查并提示更新
  Future<void> checkAndPromptUpdate(BuildContext context) async {
    final versionInfo = await versionChecker.checkForUpdate();
    
    if (versionInfo == null) {
      print('Already latest version');
      return;
    }

    if (!context.mounted) return;

    // 检测安装来源
    final isFromMarket = await AppMarketDetector.isFromAppMarket();
    
    if (isFromMarket) {
      _showMarketUpdateDialog(context, versionInfo);
    } else {
      _showSelfHostedUpdateDialog(context, versionInfo);
    }
  }

  /// 应用市场更新提示
  void _showMarketUpdateDialog(BuildContext context, VersionInfo versionInfo) {
    showDialog(
      context: context,
      barrierDismissible: !versionInfo.forceUpdate,
      builder: (context) => WillPopScope(
        onWillPop: () async => !versionInfo.forceUpdate,
        child: AlertDialog(
          title: Text(versionInfo.forceUpdate ? '发现重要更新' : '发现新版本'),
          content: SingleChildScrollView(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('最新版本: ${versionInfo.version}'),
                if (versionInfo.updateLog != null) ...[
                  const SizedBox(height: 16),
                  const Text('更新内容:', style: TextStyle(fontWeight: FontWeight.bold)),
                  const SizedBox(height: 8),
                  Text(versionInfo.updateLog!),
                ],
                const SizedBox(height: 16),
                const Text(
                  '检测到您的应用来自应用市场，建议前往应用市场更新以获得最佳体验。',
                  style: TextStyle(fontSize: 12, color: Colors.grey),
                ),
              ],
            ),
          ),
          actions: [
            if (!versionInfo.forceUpdate)
              TextButton(
                onPressed: () => Navigator.pop(context),
                child: const Text('稍后'),
              ),
            ElevatedButton(
              onPressed: () async {
                Navigator.pop(context);
                final packageInfo = await versionChecker.getCurrentVersion();
                await AppMarketDetector.openAppMarketDetail(packageInfo.packageName);
              },
              child: const Text('前往应用市场'),
            ),
          ],
        ),
      ),
    );
  }

  /// 自建更新对话框
  void _showSelfHostedUpdateDialog(BuildContext context, VersionInfo versionInfo) {
    showDialog(
      context: context,
      barrierDismissible: !versionInfo.forceUpdate,
      builder: (context) => WillPopScope(
        onWillPop: () async => !versionInfo.forceUpdate,
        child: AlertDialog(
          title: Text(versionInfo.forceUpdate ? '发现重要更新' : '发现新版本'),
          content: SingleChildScrollView(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('最新版本: ${versionInfo.version}'),
                const SizedBox(height: 8),
                Text('文件大小: ${versionInfo.fileSizeFriendly}'),
                if (versionInfo.updateLog != null) ...[
                  const SizedBox(height: 16),
                  const Text('更新内容:', style: TextStyle(fontWeight: FontWeight.bold)),
                  const SizedBox(height: 8),
                  Text(versionInfo.updateLog!),
                ],
              ],
            ),
          ),
          actions: [
            if (!versionInfo.forceUpdate)
              TextButton(
                onPressed: () => Navigator.pop(context),
                child: const Text('稍后'),
              ),
            ElevatedButton(
              onPressed: () {
                Navigator.pop(context);
                _startUpdate(context, versionInfo);
              },
              child: const Text('立即更新'),
            ),
          ],
        ),
      ),
    );
  }

  /// 开始下载并安装
  Future<void> _startUpdate(BuildContext context, VersionInfo versionInfo) async {
    if (!context.mounted) return;

    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => _DownloadProgressDialog(
        versionInfo: versionInfo,
        downloadManager: downloadManager,
      ),
    );
  }
}

/// 下载进度对话框
class _DownloadProgressDialog extends StatefulWidget {
  final VersionInfo versionInfo;
  final DownloadManager downloadManager;

  const _DownloadProgressDialog({
    required this.versionInfo,
    required this.downloadManager,
  });

  @override
  State<_DownloadProgressDialog> createState() => _DownloadProgressDialogState();
}

class _DownloadProgressDialogState extends State<_DownloadProgressDialog> {
  double _progress = 0.0;
  String _statusText = '准备下载...';
  bool _isDownloading = true;

  @override
  void initState() {
    super.initState();
    _startDownload();
  }

  Future<void> _startDownload() async {
    setState(() {
      _statusText = '正在下载...';
    });

    final apkFile = await widget.downloadManager.downloadApk(
      url: widget.versionInfo.downloadUrl,
      sha256Expected: widget.versionInfo.sha256,
      onProgress: (received, total) {
        setState(() {
          _progress = received / total;
          final receivedMB = (received / 1024 / 1024).toStringAsFixed(1);
          final totalMB = (total / 1024 / 1024).toStringAsFixed(1);
          _statusText = '正在下载... $receivedMB MB / $totalMB MB';
        });
      },
    );

    if (apkFile == null) {
      setState(() {
        _statusText = '下载失败';
        _isDownloading = false;
      });
      return;
    }

    setState(() {
      _statusText = '下载完成，准备安装...';
    });

    await Future.delayed(const Duration(milliseconds: 500));

    final installed = await ApkInstaller.installApk(apkFile);
    
    if (!mounted) return;

    if (installed) {
      Navigator.pop(context);
    } else {
      setState(() {
        _statusText = '安装失败';
        _isDownloading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async => !_isDownloading,
      child: AlertDialog(
        title: const Text('正在更新'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            LinearProgressIndicator(value: _progress),
            const SizedBox(height: 16),
            Text(_statusText),
          ],
        ),
        actions: [
          if (!_isDownloading)
            TextButton(
              onPressed: () {
                widget.downloadManager.cancelDownload();
                Navigator.pop(context);
              },
              child: const Text('关闭'),
            ),
        ],
      ),
    );
  }
}
```

---

## 六、统一升级服务

### 6.1 升级服务入口

**文件: `lib/core/updater/update_service.dart`**

```dart
import 'package:flutter/material.dart';
import 'channels/google_play_updater.dart';
import 'channels/self_hosted_updater.dart';

enum UpdateChannel {
  googlePlay,
  selfHosted,
}

class UpdateService {
  static UpdateService? _instance;
  UpdateService._();
  
  factory UpdateService() {
    _instance ??= UpdateService._();
    return _instance!;
  }

  late UpdateChannel _channel;
  late String _apiBaseUrl;
  SelfHostedUpdater? _selfHostedUpdater;

  /// 初始化
  void initialize({
    required UpdateChannel channel,
    String? apiBaseUrl,
  }) {
    _channel = channel;
    
    if (channel == UpdateChannel.selfHosted) {
      if (apiBaseUrl == null) {
        throw ArgumentError('apiBaseUrl is required for self-hosted channel');
      }
      _apiBaseUrl = apiBaseUrl;
      _selfHostedUpdater = SelfHostedUpdater(apiBaseUrl: apiBaseUrl);
    }

    print('UpdateService initialized with channel: $_channel');
  }

  /// 检查更新
  Future<void> checkForUpdate(BuildContext context) async {
    switch (_channel) {
      case UpdateChannel.googlePlay:
        await _checkGooglePlayUpdate(context);
        break;
      case UpdateChannel.selfHosted:
        await _selfHostedUpdater!.checkAndPromptUpdate(context);
        break;
    }
  }

  Future<void> _checkGooglePlayUpdate(BuildContext context) async {
    final hasUpdate = await GooglePlayUpdater.checkForUpdate();
    
    if (!hasUpdate) {
      print('No update available');
      return;
    }

    if (!context.mounted) return;

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('发现新版本'),
        content: const Text('有新版本可用，是否立即更新？'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('稍后'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.pop(context);
              GooglePlayUpdater.performFlexibleUpdate();
            },
            child: const Text('更新'),
          ),
        ],
      ),
    );
  }

  /// 手动检查更新
  Future<void> manualCheckUpdate(BuildContext context) async {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => const Center(
        child: CircularProgressIndicator(),
      ),
    );

    await Future.delayed(const Duration(seconds: 1));
    
    if (!context.mounted) return;
    Navigator.pop(context);

    await checkForUpdate(context);
  }
}
```

### 6.2 使用示例

**在 main.dart 中初始化:**

```dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  UpdateService().initialize(
    channel: UpdateChannel.selfHosted,
    apiBaseUrl: 'https://your-api.com',
  );
  
  runApp(const MyApp());
}
```

**在 App 启动时检查更新:**

```dart
class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    _checkUpdate();
  }

  Future<void> _checkUpdate() async {
    await Future.delayed(const Duration(seconds: 3));
    
    if (!mounted) return;
    
    await UpdateService().checkForUpdate(context);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(/* ... */);
  }
}
```

---

## 七、后端接口设计

### 7.1 版本检测接口

```
GET /api/app/version/check

Query Parameters:
  - platform: android | ios
  - current_version: 1.1.0
  - current_version_code: 110

Response:
{
  "version": "1.2.0",
  "versionCode": 120,
  "downloadUrl": "https://your-cdn.com/app-release-v1.2.0.apk",
  "fileSize": 26843545,
  "fileSizeFriendly": "25.6 MB",
  "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "forceUpdate": false,
  "updateLog": "1. 修复若干已知问题\n2. 优化用户体验",
  "releaseDate": "2024-01-15T10:00:00Z"
}
```

### 7.2 后端实现示例

Node.js + Express:

```javascript
app.get('/api/app/version/check', async (req, res) => {
  const { platform, current_version_code } = req.query;
  
  const latestVersion = await db.getLatestVersion(platform);
  
  if (parseInt(current_version_code) >= latestVersion.versionCode) {
    return res.json({ needUpdate: false });
  }
  
  res.json({
    needUpdate: true,
    version: latestVersion.version,
    versionCode: latestVersion.versionCode,
    downloadUrl: `https://cdn.example.com/${latestVersion.apkFile}`,
    fileSize: latestVersion.fileSize,
    fileSizeFriendly: formatFileSize(latestVersion.fileSize),
    sha256: latestVersion.sha256,
    forceUpdate: latestVersion.forceUpdate,
    updateLog: latestVersion.updateLog,
    releaseDate: latestVersion.releaseDate,
  });
});
```

### 7.3 数据库表设计

```sql
CREATE TABLE app_versions (
  id BIGSERIAL PRIMARY KEY,
  platform VARCHAR(10) NOT NULL,
  version VARCHAR(50) NOT NULL,
  version_code INTEGER NOT NULL,
  apk_file VARCHAR(255),
  download_url TEXT NOT NULL,
  file_size BIGINT NOT NULL,
  sha256 VARCHAR(64) NOT NULL,
  force_update BOOLEAN DEFAULT FALSE,
  update_log TEXT,
  release_date TIMESTAMP NOT NULL,
  is_active BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMP DEFAULT NOW(),
  
  INDEX idx_platform (platform),
  INDEX idx_version_code (version_code),
  INDEX idx_active (is_active)
);
```

---

## 八、Android 原生配置

### 8.1 MainActivity 实现

**文件: `android/app/src/main/kotlin/com/yourapp/MainActivity.kt`**

```kotlin
package com.yourapp

import android.content.Intent
import android.net.Uri
import android.os.Build
import androidx.core.content.FileProvider
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File

class MainActivity: FlutterActivity() {
    private val INSTALLER_CHANNEL = "com.yourapp/apk_installer"
    private val MARKET_CHANNEL = "com.yourapp/app_market"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        
        // APK 安装器
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, INSTALLER_CHANNEL)
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "installApk" -> {
                        val apkPath = call.argument<String>("apkPath")
                        if (apkPath != null) {
                            installApk(apkPath)
                            result.success(true)
                        } else {
                            result.error("INVALID_PATH", "APK path is null", null)
                        }
                    }
                    else -> result.notImplemented()
                }
            }
        
        // 应用市场检测
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, MARKET_CHANNEL)
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "getInstallerPackageName" -> {
                        try {
                            val installer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                                packageManager.getInstallSourceInfo(packageName).installingPackageName
                            } else {
                                @Suppress("DEPRECATION")
                                packageManager.getInstallerPackageName(packageName)
                            }
                            result.success(installer)
                        } catch (e: Exception) {
                            result.success(null)
                        }
                    }
                    else -> result.notImplemented()
                }
            }
    }

    private fun installApk(apkPath: String) {
        val apkFile = File(apkPath)
        if (!apkFile.exists()) return

        val intent = Intent(Intent.ACTION_VIEW)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK

        val apkUri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
            FileProvider.getUriForFile(
                this,
                "${applicationContext.packageName}.fileprovider",
                apkFile
            )
        } else {
            Uri.fromFile(apkFile)
        }

        intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
        startActivity(intent)
    }
}
```

### 8.2 Gradle Flavor 配置

**文件: `android/app/build.gradle`**

```gradle
android {
    // 其他配置...

    flavorDimensions "channel"
    
    productFlavors {
        googleplay {
            dimension "channel"
            applicationIdSuffix ".googleplay"
            versionNameSuffix "-gp"
        }
        
        china {
            dimension "channel"
            versionNameSuffix "-china"
        }
    }
}
```

### 8.3 Google Play 版本 Manifest

**文件: `android/app/src/googleplay/AndroidManifest.xml`**

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    
    <application>
        <!-- Google Play 版本不需要 FileProvider -->
    </application>
</manifest>
```

### 8.4 自建渠道 Manifest

**文件: `android/app/src/china/AndroidManifest.xml`**

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
    
    <application>
        <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>
    </application>
</manifest>
```

### 8.5 FileProvider 路径配置

**文件: `android/app/src/main/res/xml/file_paths.xml`**

```xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path name="internal_files" path="." />
    <cache-path name="cache" path="." />
</paths>
```

### 8.6 构建命令

```bash
# Google Play 版本
flutter build apk --flavor googleplay --dart-define=CHANNEL=googleplay

# 国内版本
flutter build apk --flavor china --dart-define=CHANNEL=china
```

---

## 九、安全性考虑

### 9.1 HTTPS 强制

所有下载链接必须使用 HTTPS 协议：

```dart
if (!downloadUrl.startsWith('https://')) {
  throw Exception('Download URL must use HTTPS');
}
```

### 9.2 SHA-256 完整性校验

下载完成后必须校验文件完整性：

```dart
final isValid = await _verifySha256(apkFile, expectedSha256);
if (!isValid) {
  await apkFile.delete();
  throw Exception('SHA-256 verification failed');
}
```

### 9.3 渠道隔离

- Google Play 版本：不包含任何安装相关权限和代码
- 自建渠道版本：包含完整的下载安装功能
- 使用 Gradle Flavor 在构建层面完全隔离

### 9.4 应用市场策略

检测应用来源，避免与应用市场升级机制冲突：

```dart
final isFromMarket = await AppMarketDetector.isFromAppMarket();
if (isFromMarket) {
  // 引导用户去应用市场更新
  AppMarketDetector.openAppMarketDetail(packageName);
} else {
  // 使用自建更新
  selfHostedUpdate();
}
```

---

## 十、实施步骤

### Step 1: 添加依赖

```bash
flutter pub add package_info_plus dio in_app_update permission_handler path_provider crypto url_launcher
```

### Step 2: 创建目录结构

```bash
mkdir -p lib/core/updater/{models,channels}
```

### Step 3: 创建 Flutter 代码

按照本文档创建以下文件：
- models/version_info.dart
- version_checker.dart
- download_manager.dart
- apk_installer.dart
- app_market_detector.dart
- channels/google_play_updater.dart
- channels/self_hosted_updater.dart
- update_service.dart

### Step 4: 生成 JSON 序列化代码

```bash
flutter pub run build_runner build --delete-conflicting-outputs
```

### Step 5: 配置 Android 原生代码

- 修改 MainActivity.kt
- 配置 build.gradle
- 创建 Flavor 专属 AndroidManifest.xml
- 添加 file_paths.xml

### Step 6: 实现后端 API

实现版本检测接口，返回最新版本信息。

### Step 7: 初始化服务

在 main.dart 中初始化 UpdateService。

### Step 8: 构建测试

```bash
# Google Play 版本
flutter build apk --flavor googleplay --dart-define=CHANNEL=googleplay --release

# 国内版本
flutter build apk --flavor china --dart-define=CHANNEL=china --release
```

### Step 9: 验证权限

```bash
# 检查 Google Play 版本（不应有安装权限）
aapt dump permissions build/app/outputs/flutter-apk/app-googleplay-release.apk | grep INSTALL

# 检查国内版本（应有安装权限）
aapt dump permissions build/app/outputs/flutter-apk/app-china-release.apk | grep INSTALL
```

### Step 10: 测试验证

- 测试下载流程
- 测试应用市场检测
- 测试强制更新
- 测试断网场景

---

## 附录

### A. 常见问题

| 问题 | 解决方案 |
|------|---------|
| 下载失败 | 检查网络、确保 HTTPS、添加重试机制 |
| 无法安装 | 确认 china flavor 有安装权限 |
| SHA-256 校验失败 | 重新下载、检查网络稳定性 |
| Google Play 更新不显示 | 确保 versionCode 递增 |

### B. 调试命令

```bash
# 查看 APK 权限
aapt dump permissions app-release.apk

# 查看 APK 包名
aapt dump badging app-release.apk | grep package

# 生成 SHA-256
sha256sum app-release.apk
```

### C. 参考资源

- [Flutter 官方文档](https://flutter.dev/docs)
- [in_app_update 插件](https://pub.dev/packages/in_app_update)
- [Android FileProvider 指南](https://developer.android.com/reference/androidx/core/content/FileProvider)

---

文档版本: 2.0  
最后更新: 2024-01-15
