chore(merge): 合并 iOS 无线数据询问
This commit is contained in:
commit
98d7debfb5
6 files changed
+199
-1
No files matched your search
@@ -11,6 +11,7 @@
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
8C4E1A022F9A4B1000C636F3 /* WirelessDataPrompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C4E1A012F9A4B1000C636F3 /* WirelessDataPrompt.swift */; };
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
@@ -49,6 +50,7 @@
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
8C4E1A012F9A4B1000C636F3 /* WirelessDataPrompt.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WirelessDataPrompt.swift; sourceTree = "<group>"; };
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
@@ -122,6 +124,7 @@
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
8C4E1A012F9A4B1000C636F3 /* WirelessDataPrompt.swift */,
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
@@ -282,6 +285,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
8C4E1A022F9A4B1000C636F3 /* WirelessDataPrompt.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
|
||||
);
|
||||
|
||||
@@ -37,6 +37,7 @@ final class RunnerFlutterViewController: FlutterViewController {
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
private let wirelessDataPrompt = WirelessDataPrompt()
|
||||
private let thumbnailQueue = DispatchQueue(
|
||||
label: "com.stormg.lumadrama.thumbnail",
|
||||
qos: .userInitiated
|
||||
@@ -52,6 +53,7 @@ final class RunnerFlutterViewController: FlutterViewController {
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
registerWirelessDataChannel(engineBridge)
|
||||
let channel = FlutterMethodChannel(
|
||||
name: "com.stormg.lumadrama/share",
|
||||
binaryMessenger: engineBridge.applicationRegistrar.messenger()
|
||||
@@ -132,4 +134,22 @@ final class RunnerFlutterViewController: FlutterViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func registerWirelessDataChannel(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
let channel = FlutterMethodChannel(
|
||||
name: WirelessDataPrompt.channelName,
|
||||
binaryMessenger: engineBridge.applicationRegistrar.messenger()
|
||||
)
|
||||
channel.setMethodCallHandler { [weak self] call, result in
|
||||
guard call.method == WirelessDataPrompt.awaitDecisionMethod else {
|
||||
result(FlutterMethodNotImplemented)
|
||||
return
|
||||
}
|
||||
guard let self else {
|
||||
result(nil)
|
||||
return
|
||||
}
|
||||
self.wirelessDataPrompt.awaitDecision(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import CoreTelephony
|
||||
import Flutter
|
||||
import Foundation
|
||||
|
||||
/// 监听系统无线数据选择,供 Flutter 在首个接口请求前等待。
|
||||
final class WirelessDataPrompt {
|
||||
static let channelName = "com.stormg.lumadrama/wireless-data"
|
||||
static let awaitDecisionMethod = "awaitDecision"
|
||||
|
||||
private let cellularData = CTCellularData()
|
||||
private let lock = NSLock()
|
||||
private var pending: [FlutterResult] = []
|
||||
private var decided = false
|
||||
|
||||
init() {
|
||||
cellularData.cellularDataRestrictionDidUpdateNotifier = { [weak self] state in
|
||||
DispatchQueue.main.async {
|
||||
self?.completeIfDecided(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 状态未选择时先挂起;用户点选后完成全部等待。
|
||||
func awaitDecision(_ result: @escaping FlutterResult) {
|
||||
lock.lock()
|
||||
if decided {
|
||||
lock.unlock()
|
||||
DispatchQueue.main.async {
|
||||
result(nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
pending.append(result)
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
private func completeIfDecided(_ state: CTCellularDataRestrictedState) {
|
||||
if state == .restrictedStateUnknown {
|
||||
return
|
||||
}
|
||||
lock.lock()
|
||||
if decided {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
decided = true
|
||||
let waiters = pending
|
||||
pending.removeAll()
|
||||
lock.unlock()
|
||||
waiters.forEach { $0(nil) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// iOS 首次启动等待系统无线数据选择,再发首个接口请求。
|
||||
abstract final class WirelessDataPrompt {
|
||||
const WirelessDataPrompt._();
|
||||
|
||||
/// 系统选择超过此时长仍未回调时继续启动。
|
||||
static const Duration decisionTimeout = Duration(seconds: 15);
|
||||
|
||||
/// 与 iOS `WirelessDataPrompt.channelName` 保持一致。
|
||||
static const String channelName = 'com.stormg.lumadrama/wireless-data';
|
||||
|
||||
static const String _awaitDecisionMethod = 'awaitDecision';
|
||||
|
||||
static const MethodChannel _channel = MethodChannel(channelName);
|
||||
|
||||
static Future<void>? _pending;
|
||||
|
||||
/// 非 iOS 立即返回。iOS 只等待一次,失败或超时后放行。
|
||||
static Future<void> awaitDecision({
|
||||
required bool enabled,
|
||||
Future<void> Function()? ask,
|
||||
Duration? timeout,
|
||||
}) {
|
||||
if (!enabled) return Future<void>.value();
|
||||
return _pending ??= _askOnce(ask, timeout ?? decisionTimeout);
|
||||
}
|
||||
|
||||
/// 测试隔离用,清掉已完成的等待。
|
||||
@visibleForTesting
|
||||
static void resetForTest() {
|
||||
_pending = null;
|
||||
}
|
||||
|
||||
static Future<void> _askOnce(
|
||||
Future<void> Function()? ask,
|
||||
Duration timeout,
|
||||
) async {
|
||||
try {
|
||||
await (ask ?? _invokeChannel).call().timeout(timeout);
|
||||
} on TimeoutException {
|
||||
_log('decision timed out');
|
||||
} catch (error) {
|
||||
_log('decision skipped: $error');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _invokeChannel() {
|
||||
return _channel.invokeMethod<void>(_awaitDecisionMethod);
|
||||
}
|
||||
|
||||
static void _log(String message) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('[wireless-data] $message');
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -2,17 +2,21 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'app.dart';
|
||||
import 'core/config/app_config.dart';
|
||||
import 'core/config/server_endpoint.dart';
|
||||
import 'core/platform/wireless_data_prompt.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await ServerEndpoint.load();
|
||||
AppConfig.ready = AppConfig.probeAndApplyBaseUrl();
|
||||
AppConfig.ready = WirelessDataPrompt.awaitDecision(
|
||||
enabled: !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS,
|
||||
).then((_) => AppConfig.probeAndApplyBaseUrl());
|
||||
runApp(const LumaDramaApp());
|
||||
unawaited(
|
||||
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]),
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/platform/wireless_data_prompt.dart';
|
||||
|
||||
void main() {
|
||||
setUp(WirelessDataPrompt.resetForTest);
|
||||
tearDown(WirelessDataPrompt.resetForTest);
|
||||
|
||||
test('should_return_immediately_when_disabled', () async {
|
||||
var calls = 0;
|
||||
|
||||
await WirelessDataPrompt.awaitDecision(
|
||||
enabled: false,
|
||||
ask: () async {
|
||||
calls++;
|
||||
},
|
||||
);
|
||||
|
||||
expect(calls, 0);
|
||||
});
|
||||
|
||||
test('should_ask_once_when_enabled', () async {
|
||||
var calls = 0;
|
||||
|
||||
Future<void> ask() async {
|
||||
calls++;
|
||||
}
|
||||
|
||||
await WirelessDataPrompt.awaitDecision(enabled: true, ask: ask);
|
||||
await WirelessDataPrompt.awaitDecision(enabled: true, ask: ask);
|
||||
|
||||
expect(calls, 1);
|
||||
});
|
||||
|
||||
test('should_continue_when_decision_times_out', () async {
|
||||
await WirelessDataPrompt.awaitDecision(
|
||||
enabled: true,
|
||||
ask: () => Completer<void>().future,
|
||||
timeout: const Duration(milliseconds: 20),
|
||||
);
|
||||
});
|
||||
|
||||
test('should_continue_when_ask_fails', () async {
|
||||
await WirelessDataPrompt.awaitDecision(
|
||||
enabled: true,
|
||||
ask: () async {
|
||||
throw StateError('channel missing');
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user