2026-09-19 03:42:49 +08:00
|
|
|
|
/// 上传传输抽象与双实现(P3 计划 Task 7;Task 8 扩展 iOS 选择分支)。
|
2026-09-19 03:22:42 +08:00
|
|
|
|
///
|
|
|
|
|
|
/// 引擎的传输步骤(presign PUT)经 [Transport] 接口委托:
|
|
|
|
|
|
/// - **[DartHttpTransport]**(Task 6 逻辑原样抽出):裸 dio 流式 PUT +
|
2026-09-19 03:42:49 +08:00
|
|
|
|
/// 进度计数流 + 取消令牌——桌面 / 调试路径(dart-define
|
2026-09-19 03:22:42 +08:00
|
|
|
|
/// `FORCE_DART_TRANSPORT=true` 强制启用);
|
2026-09-19 03:42:49 +08:00
|
|
|
|
/// - **[NativeTransport]**(Task 7 新增,Android;Task 8 起含 iOS):
|
|
|
|
|
|
/// MethodChannel `photolive/uploads` 注册原生后台上传——Android =
|
|
|
|
|
|
/// WorkManager 后台工作(expedited + dataSync 前台服务);iOS =
|
|
|
|
|
|
/// URLSession background(identifier com.photolive.photo_live.uploads,
|
|
|
|
|
|
/// 编译留 G3)。两平台协议逐字一致(Kotlin `UploadProtocol.kt` /
|
|
|
|
|
|
/// Swift `UploadBridge.swift`),EventChannel `photolive/uploads/events`
|
|
|
|
|
|
/// 回流 progress/completed/failed/needsRenew 事件——App 切后台/锁屏后
|
|
|
|
|
|
/// 续传由系统调度承担。
|
2026-09-19 03:22:42 +08:00
|
|
|
|
///
|
|
|
|
|
|
/// 失败语义(两实现归一为 [TransportException] 族,引擎分类见
|
|
|
|
|
|
/// upload_engine.dart `_classifyFailure`):
|
|
|
|
|
|
/// | 情形 | 异常 |
|
|
|
|
|
|
/// |---|---|
|
|
|
|
|
|
/// | 网络不可达/超时/取消令牌触发前的连接层失败 | `TransportException(network)` |
|
|
|
|
|
|
/// | HTTP 403(presign 凭证过期) | [TransportRenewRequiredException](引擎 renew 后重发) |
|
|
|
|
|
|
/// | 其余 HTTP 非 2xx | `TransportException(http, statusCode)` |
|
|
|
|
|
|
/// | 源文件缺失(原生 worker 发现) | `TransportException(sourceMissing)` |
|
|
|
|
|
|
/// | 平台通道调用失败 | `TransportException(channel)` |
|
|
|
|
|
|
/// | 取消令牌触发 | `TransportException(cancelled)`(引擎静默收敛) |
|
|
|
|
|
|
library;
|
|
|
|
|
|
|
|
|
|
|
|
import 'dart:async';
|
|
|
|
|
|
import 'dart:io';
|
|
|
|
|
|
|
|
|
|
|
|
import 'package:dio/dio.dart';
|
|
|
|
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
|
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
|
|
|
|
|
|
|
|
/// presign PUT 传输接口(引擎只委托文件 PUT;init/complete/renew 仍在
|
|
|
|
|
|
/// Dart 引擎内,保持服务端交互语义单源)。
|
|
|
|
|
|
abstract interface class Transport {
|
|
|
|
|
|
/// 单次文件 PUT。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// - 成功返回 [TransportResult](ETag 供诊断;完成核验以服务端为准);
|
|
|
|
|
|
/// - 失败抛 [TransportException] 族(见库级注释分类表);
|
|
|
|
|
|
/// - [cancelToken] 取消时抛 `TransportException(cancelled)`。
|
|
|
|
|
|
Future<TransportResult> send({
|
|
|
|
|
|
required String taskId,
|
|
|
|
|
|
required String url,
|
|
|
|
|
|
required String method,
|
|
|
|
|
|
required Map<String, String> headers,
|
|
|
|
|
|
required String filePath,
|
|
|
|
|
|
Map<String, Object?>? metadata,
|
|
|
|
|
|
void Function(int sent, int total)? onProgress,
|
|
|
|
|
|
CancelToken? cancelToken,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/// 释放底层资源(引擎 dispose 调用;注入方自持传输时自行管理)。
|
|
|
|
|
|
void dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// PUT 成功结果。
|
|
|
|
|
|
class TransportResult {
|
|
|
|
|
|
const TransportResult({this.etag});
|
|
|
|
|
|
|
|
|
|
|
|
/// 服务端返回的 ETag 响应头(可空;仅诊断用途)。
|
|
|
|
|
|
final String? etag;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 传输失败码(原生侧事件码与本侧异常码的归一层)。
|
|
|
|
|
|
abstract final class TransportErrorCodes {
|
|
|
|
|
|
/// 网络不可达/超时/连接层失败。
|
|
|
|
|
|
static const String network = 'NETWORK';
|
|
|
|
|
|
|
|
|
|
|
|
/// HTTP 层失败(statusCode 保留在异常上)。
|
|
|
|
|
|
static const String http = 'HTTP';
|
|
|
|
|
|
|
|
|
|
|
|
/// 取消令牌触发(非失败分类;引擎静默收敛)。
|
|
|
|
|
|
static const String cancelled = 'CANCELLED';
|
|
|
|
|
|
|
|
|
|
|
|
/// 源文件缺失(受管副本被清理,不可恢复)。
|
|
|
|
|
|
static const String sourceMissing = 'SOURCE_MISSING';
|
|
|
|
|
|
|
|
|
|
|
|
/// 平台通道调用失败(桥接层异常)。
|
|
|
|
|
|
static const String channel = 'CHANNEL';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 传输失败(两实现统一的异常类型)。
|
|
|
|
|
|
class TransportException implements Exception {
|
|
|
|
|
|
const TransportException(this.code, {this.statusCode, this.cause});
|
|
|
|
|
|
|
|
|
|
|
|
/// 失败分类([TransportErrorCodes])。
|
|
|
|
|
|
final String code;
|
|
|
|
|
|
|
|
|
|
|
|
/// HTTP 状态码(http 类失败携带)。
|
|
|
|
|
|
final int? statusCode;
|
|
|
|
|
|
|
|
|
|
|
|
/// 底层异常(诊断)。
|
|
|
|
|
|
final Object? cause;
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
String toString() =>
|
|
|
|
|
|
'TransportException($code${statusCode != null ? ', status=$statusCode' : ''}'
|
|
|
|
|
|
'${cause != null ? ', cause=$cause' : ''})';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// HTTP 403(presign 凭证过期):引擎 renewUpload 换新凭证后重发。
|
|
|
|
|
|
class TransportRenewRequiredException extends TransportException {
|
|
|
|
|
|
const TransportRenewRequiredException({super.cause})
|
|
|
|
|
|
: super(TransportErrorCodes.http, statusCode: 403);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Dart(dio)实现 ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// Task 6 的裸 dio 直传实现(iOS / 桌面 / FORCE_DART_TRANSPORT 调试)。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 严格携带 credential.headers(+ Content-Length——流式发送 dio 无法自动
|
|
|
|
|
|
/// 推断,显式声明);进度经计数流上报(不依赖适配器对 onSendProgress 的
|
|
|
|
|
|
/// 实现)。
|
|
|
|
|
|
class DartHttpTransport implements Transport {
|
|
|
|
|
|
DartHttpTransport({Dio? dio})
|
|
|
|
|
|
: _dio = dio,
|
|
|
|
|
|
_ownedDio = dio == null;
|
|
|
|
|
|
|
|
|
|
|
|
final Dio? _dio;
|
|
|
|
|
|
final bool _ownedDio;
|
|
|
|
|
|
|
|
|
|
|
|
/// 自建实例惰性创建(注入 dio 时不建)。
|
|
|
|
|
|
Dio? _created;
|
|
|
|
|
|
Dio get _client => _dio ?? (_created ??= Dio());
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Future<TransportResult> send({
|
|
|
|
|
|
required String taskId,
|
|
|
|
|
|
required String url,
|
|
|
|
|
|
required String method,
|
|
|
|
|
|
required Map<String, String> headers,
|
|
|
|
|
|
required String filePath,
|
|
|
|
|
|
Map<String, Object?>? metadata,
|
|
|
|
|
|
void Function(int sent, int total)? onProgress,
|
|
|
|
|
|
CancelToken? cancelToken,
|
|
|
|
|
|
}) async {
|
|
|
|
|
|
final file = File(filePath);
|
|
|
|
|
|
final length = await file.length();
|
|
|
|
|
|
var sent = 0;
|
|
|
|
|
|
final progressStream = file.openRead().map((List<int> chunk) {
|
|
|
|
|
|
sent += chunk.length;
|
|
|
|
|
|
onProgress?.call(sent, length);
|
|
|
|
|
|
return chunk;
|
|
|
|
|
|
});
|
|
|
|
|
|
try {
|
|
|
|
|
|
final resp = await _client.put<void>(
|
|
|
|
|
|
url,
|
|
|
|
|
|
data: progressStream,
|
|
|
|
|
|
cancelToken: cancelToken,
|
|
|
|
|
|
options: Options(
|
|
|
|
|
|
method: method,
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
...headers,
|
|
|
|
|
|
Headers.contentLengthHeader: length,
|
|
|
|
|
|
},
|
|
|
|
|
|
// 响应体非 JSON(MinIO XML/空),防 transformer 解析报错。
|
|
|
|
|
|
responseType: ResponseType.plain,
|
|
|
|
|
|
receiveDataWhenStatusError: true,
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
final etag = resp.headers.value(HttpHeaders.etagHeader);
|
|
|
|
|
|
return TransportResult(etag: etag);
|
|
|
|
|
|
} on DioException catch (e) {
|
|
|
|
|
|
throw _mapDioException(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// dio 异常 → 传输异常归一(403 → renew;连接层 → network;取消 →
|
|
|
|
|
|
/// cancelled;其余 badResponse → http)。
|
|
|
|
|
|
static TransportException _mapDioException(DioException e) {
|
|
|
|
|
|
if (e.type == DioExceptionType.cancel) {
|
|
|
|
|
|
return const TransportException(TransportErrorCodes.cancelled);
|
|
|
|
|
|
}
|
|
|
|
|
|
final status = e.response?.statusCode;
|
|
|
|
|
|
if (status != null) {
|
|
|
|
|
|
if (status == 403) return const TransportRenewRequiredException();
|
|
|
|
|
|
return TransportException(TransportErrorCodes.http,
|
|
|
|
|
|
statusCode: status, cause: e);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (e.type == DioExceptionType.connectionTimeout ||
|
|
|
|
|
|
e.type == DioExceptionType.sendTimeout ||
|
|
|
|
|
|
e.type == DioExceptionType.receiveTimeout ||
|
|
|
|
|
|
e.type == DioExceptionType.transformTimeout ||
|
|
|
|
|
|
e.type == DioExceptionType.connectionError ||
|
|
|
|
|
|
e.type == DioExceptionType.badCertificate ||
|
|
|
|
|
|
e.type == DioExceptionType.unknown) {
|
|
|
|
|
|
return TransportException(TransportErrorCodes.network, cause: e);
|
|
|
|
|
|
}
|
|
|
|
|
|
// badResponse 无响应必经上方分支,防御性兜底为网络类。
|
|
|
|
|
|
return TransportException(TransportErrorCodes.network, cause: e);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void dispose() {
|
|
|
|
|
|
if (_ownedDio) {
|
|
|
|
|
|
_client.close();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 03:42:49 +08:00
|
|
|
|
// ── 原生(Android WorkManager / iOS URLSession background)实现 ─────────
|
2026-09-19 03:22:42 +08:00
|
|
|
|
|
2026-09-19 03:42:49 +08:00
|
|
|
|
/// 平台通道名(与 Android `UploadPlugin.kt` / iOS `UploadPlugin.swift`
|
|
|
|
|
|
/// 一致;原生常量见 UploadProtocol.kt 与 UploadBridge.swift——Task 8 起
|
|
|
|
|
|
/// iOS 为同一协议的 Swift 镜像,无协议分歧)。
|
2026-09-19 03:22:42 +08:00
|
|
|
|
abstract final class UploadBridgeChannels {
|
|
|
|
|
|
/// 命令通道:registerTask / cancelTask / queryTask / ackEvents。
|
|
|
|
|
|
static const String methods = 'photolive/uploads';
|
|
|
|
|
|
|
|
|
|
|
|
/// 事件流:progress / completed / failed / needsRenew(带 seq,
|
|
|
|
|
|
/// 终态事件未 ack 前原生侧缓存重放——生命周期安全)。
|
|
|
|
|
|
static const String events = 'photolive/uploads/events';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 平台通道绑定薄层(默认真实通道;测试注入替身)。
|
|
|
|
|
|
class NativeUploadChannel {
|
|
|
|
|
|
const NativeUploadChannel();
|
|
|
|
|
|
|
|
|
|
|
|
static const MethodChannel _methods = MethodChannel(
|
|
|
|
|
|
UploadBridgeChannels.methods,
|
|
|
|
|
|
);
|
|
|
|
|
|
static const EventChannel _events = EventChannel(
|
|
|
|
|
|
UploadBridgeChannels.events,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
Future<Object?> invoke(String method, Map<String, Object?> args) =>
|
|
|
|
|
|
_methods.invokeMethod<Object?>(method, args);
|
|
|
|
|
|
|
|
|
|
|
|
Stream<dynamic> get events => _events.receiveBroadcastStream();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 原生事件等待句柄。
|
|
|
|
|
|
class _NativeWaiter {
|
|
|
|
|
|
_NativeWaiter(this.onProgress);
|
|
|
|
|
|
|
|
|
|
|
|
final void Function(int sent, int total)? onProgress;
|
|
|
|
|
|
final Completer<TransportResult> completer = Completer<TransportResult>();
|
|
|
|
|
|
|
|
|
|
|
|
void complete(TransportResult result) {
|
|
|
|
|
|
if (!completer.isCompleted) completer.complete(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void completeError(Object error) {
|
|
|
|
|
|
if (!completer.isCompleted) completer.completeError(error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 03:42:49 +08:00
|
|
|
|
/// Android/iOS 原生传输:registerTask 入队原生后台上传(Android =
|
|
|
|
|
|
/// WorkManager;iOS = URLSession background),事件流回流终态/进度。
|
2026-09-19 03:22:42 +08:00
|
|
|
|
///
|
|
|
|
|
|
/// 生命周期安全:事件带单调 seq;终态事件在 Dart ack 前由原生侧缓存,
|
2026-09-19 03:42:49 +08:00
|
|
|
|
/// 重新 listen 时重放——进程重启/切后台恢复后不丢完成信号(iOS 侧缓存
|
|
|
|
|
|
/// 另落盘,系统后台拉起投递的终态在进程重启后同样重放)。进度事件不
|
2026-09-19 03:22:42 +08:00
|
|
|
|
/// 重放(瞬态)。
|
|
|
|
|
|
class NativeTransport implements Transport {
|
|
|
|
|
|
NativeTransport({NativeUploadChannel? channel})
|
|
|
|
|
|
: _channel = channel ?? const NativeUploadChannel();
|
|
|
|
|
|
|
|
|
|
|
|
final NativeUploadChannel _channel;
|
|
|
|
|
|
|
|
|
|
|
|
StreamSubscription<dynamic>? _eventsSub;
|
|
|
|
|
|
final Map<String, _NativeWaiter> _waiters = <String, _NativeWaiter>{};
|
|
|
|
|
|
|
|
|
|
|
|
void _ensureSubscribed() {
|
|
|
|
|
|
if (_eventsSub != null) return;
|
|
|
|
|
|
_eventsSub = _channel.events.listen(
|
|
|
|
|
|
_onEvent,
|
|
|
|
|
|
onError: (Object _) {
|
|
|
|
|
|
// 事件流出错:丢弃订阅,下次 send 重建(注册重试语义兜底)。
|
|
|
|
|
|
_eventsSub?.cancel();
|
|
|
|
|
|
_eventsSub = null;
|
|
|
|
|
|
},
|
|
|
|
|
|
onDone: () => _eventsSub = null,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _onEvent(dynamic raw) {
|
|
|
|
|
|
if (raw is! Map) return;
|
|
|
|
|
|
final event = Map<String, Object?>.from(raw);
|
|
|
|
|
|
final taskId = event['taskId'] as String?;
|
|
|
|
|
|
final waiter = taskId == null ? null : _waiters[taskId];
|
|
|
|
|
|
switch (event['event']) {
|
|
|
|
|
|
case 'progress':
|
|
|
|
|
|
final sent = (event['sent'] as num?)?.toInt();
|
|
|
|
|
|
final total = (event['total'] as num?)?.toInt();
|
|
|
|
|
|
if (waiter != null && sent != null && total != null) {
|
|
|
|
|
|
waiter.onProgress?.call(sent, total);
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'completed':
|
|
|
|
|
|
waiter?.complete(
|
|
|
|
|
|
TransportResult(etag: event['etag'] as String?),
|
|
|
|
|
|
);
|
|
|
|
|
|
_ackTerminal(event);
|
|
|
|
|
|
case 'needsRenew':
|
|
|
|
|
|
waiter?.completeError(const TransportRenewRequiredException());
|
|
|
|
|
|
_ackTerminal(event);
|
|
|
|
|
|
case 'failed':
|
|
|
|
|
|
waiter?.completeError(_mapFailedEvent(event));
|
|
|
|
|
|
_ackTerminal(event);
|
|
|
|
|
|
default:
|
|
|
|
|
|
break; // 未知事件:前向兼容忽略。
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 原生失败码 → 传输异常。
|
|
|
|
|
|
static TransportException _mapFailedEvent(Map<String, Object?> event) {
|
|
|
|
|
|
final status = (event['status'] as num?)?.toInt();
|
|
|
|
|
|
switch (event['code']) {
|
|
|
|
|
|
case 'NETWORK':
|
|
|
|
|
|
return const TransportException(TransportErrorCodes.network);
|
|
|
|
|
|
case 'SOURCE_MISSING':
|
|
|
|
|
|
return const TransportException(TransportErrorCodes.sourceMissing);
|
|
|
|
|
|
case 'HTTP_4XX':
|
|
|
|
|
|
case 'SERVER_ERROR':
|
|
|
|
|
|
return TransportException(
|
|
|
|
|
|
TransportErrorCodes.http,
|
|
|
|
|
|
statusCode: status,
|
|
|
|
|
|
);
|
|
|
|
|
|
default:
|
|
|
|
|
|
return TransportException(
|
|
|
|
|
|
event['code'] as String? ?? TransportErrorCodes.channel,
|
|
|
|
|
|
statusCode: status,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _ackTerminal(Map<String, Object?> event) {
|
|
|
|
|
|
final seq = (event['seq'] as num?)?.toInt();
|
|
|
|
|
|
if (seq == null) return;
|
|
|
|
|
|
unawaited(
|
|
|
|
|
|
_channel
|
|
|
|
|
|
.invoke('ackEvents', <String, Object?>{'seq': seq})
|
|
|
|
|
|
.catchError((Object _) => null),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Future<TransportResult> send({
|
|
|
|
|
|
required String taskId,
|
|
|
|
|
|
required String url,
|
|
|
|
|
|
required String method,
|
|
|
|
|
|
required Map<String, String> headers,
|
|
|
|
|
|
required String filePath,
|
|
|
|
|
|
Map<String, Object?>? metadata,
|
|
|
|
|
|
void Function(int sent, int total)? onProgress,
|
|
|
|
|
|
CancelToken? cancelToken,
|
|
|
|
|
|
}) async {
|
|
|
|
|
|
_ensureSubscribed();
|
|
|
|
|
|
final waiter = _NativeWaiter(onProgress);
|
|
|
|
|
|
_waiters[taskId] = waiter;
|
|
|
|
|
|
// 取消令牌联动:cancelTask(原生尽力停止 worker)+ 等待句柄立即失败。
|
|
|
|
|
|
cancelToken?.whenCancel.then((_) {
|
|
|
|
|
|
// 已终结则 no-op(isCompleted 守护)。
|
|
|
|
|
|
waiter.completeError(
|
|
|
|
|
|
const TransportException(TransportErrorCodes.cancelled),
|
|
|
|
|
|
);
|
|
|
|
|
|
unawaited(
|
|
|
|
|
|
_channel
|
|
|
|
|
|
.invoke('cancelTask', <String, Object?>{'taskId': taskId})
|
|
|
|
|
|
.catchError((Object _) => null),
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
try {
|
|
|
|
|
|
await _channel.invoke('registerTask', <String, Object?>{
|
|
|
|
|
|
'taskId': taskId,
|
|
|
|
|
|
'url': url,
|
|
|
|
|
|
'method': method,
|
|
|
|
|
|
'headers': headers,
|
|
|
|
|
|
'filePath': filePath,
|
|
|
|
|
|
'metadata': metadata ?? const <String, Object?>{},
|
|
|
|
|
|
});
|
|
|
|
|
|
} on PlatformException catch (e) {
|
|
|
|
|
|
// 注册失败:摘除等待句柄后归一为通道异常。
|
|
|
|
|
|
if (_waiters[taskId] == waiter) _waiters.remove(taskId);
|
|
|
|
|
|
throw TransportException(TransportErrorCodes.channel, cause: e);
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
return await waiter.completer.future;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (_waiters[taskId] == waiter) _waiters.remove(taskId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 查询原生侧工作状态(WorkInfo 概览;Task 9/11 校准用)。
|
|
|
|
|
|
Future<Map<String, Object?>?> queryTask(String taskId) async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
final result = await _channel
|
|
|
|
|
|
.invoke('queryTask', <String, Object?>{'taskId': taskId});
|
|
|
|
|
|
return result == null
|
|
|
|
|
|
? null
|
|
|
|
|
|
: Map<String, Object?>.from(result as Map<dynamic, dynamic>);
|
|
|
|
|
|
} on PlatformException {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void dispose() {
|
|
|
|
|
|
_eventsSub?.cancel();
|
|
|
|
|
|
_eventsSub = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 传输选择 ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-09-19 03:42:49 +08:00
|
|
|
|
/// dart-define FORCE_DART_TRANSPORT=true:Android/iOS 也强制 Dart dio
|
|
|
|
|
|
/// 路径(桥接层问题排查用)。
|
2026-09-19 03:22:42 +08:00
|
|
|
|
const bool kForceDartTransport = bool.fromEnvironment('FORCE_DART_TRANSPORT');
|
|
|
|
|
|
|
2026-09-19 03:42:49 +08:00
|
|
|
|
/// 传输选择纯函数(可测):Android 或 iOS 且未强制 Dart → 原生。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Task 8 起含 iOS:协议与 Android 逐字一致(同一 [NativeTransport]
|
|
|
|
|
|
/// 经同一通道名驱动 Swift 侧 UploadPlugin——iOS 编译/真机验证留 G3,
|
|
|
|
|
|
/// 代码路径在此之前即纳入原生选择)。
|
2026-09-19 03:22:42 +08:00
|
|
|
|
bool preferNativeTransport({
|
|
|
|
|
|
required bool isAndroid,
|
2026-09-19 03:42:49 +08:00
|
|
|
|
required bool isIOS,
|
2026-09-19 03:22:42 +08:00
|
|
|
|
bool forceDartTransport = kForceDartTransport,
|
|
|
|
|
|
}) =>
|
2026-09-19 03:42:49 +08:00
|
|
|
|
(isAndroid || isIOS) && !forceDartTransport;
|
2026-09-19 03:22:42 +08:00
|
|
|
|
|
2026-09-19 03:42:49 +08:00
|
|
|
|
/// 默认传输解析:Android/iOS → [NativeTransport](WorkManager /
|
|
|
|
|
|
/// URLSession background 后台续传);其余平台 / 强制覆盖 →
|
|
|
|
|
|
/// [DartHttpTransport]。
|
2026-09-19 03:22:42 +08:00
|
|
|
|
Transport defaultUploadTransport() {
|
2026-09-19 03:42:49 +08:00
|
|
|
|
if (preferNativeTransport(
|
|
|
|
|
|
isAndroid: !kIsWeb && Platform.isAndroid,
|
|
|
|
|
|
isIOS: !kIsWeb && Platform.isIOS,
|
|
|
|
|
|
)) {
|
2026-09-19 03:22:42 +08:00
|
|
|
|
return NativeTransport();
|
|
|
|
|
|
}
|
|
|
|
|
|
return DartHttpTransport();
|
|
|
|
|
|
}
|