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 */; };
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
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 */; };
|
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
||||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
@@ -122,6 +124,7 @@
|
|||||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
|
8C4E1A012F9A4B1000C636F3 /* WirelessDataPrompt.swift */,
|
||||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
);
|
);
|
||||||
@@ -282,6 +285,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
8C4E1A022F9A4B1000C636F3 /* WirelessDataPrompt.swift in Sources */,
|
||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
|
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ final class RunnerFlutterViewController: FlutterViewController {
|
|||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
|
private let wirelessDataPrompt = WirelessDataPrompt()
|
||||||
private let thumbnailQueue = DispatchQueue(
|
private let thumbnailQueue = DispatchQueue(
|
||||||
label: "com.stormg.lumadrama.thumbnail",
|
label: "com.stormg.lumadrama.thumbnail",
|
||||||
qos: .userInitiated
|
qos: .userInitiated
|
||||||
@@ -52,6 +53,7 @@ final class RunnerFlutterViewController: FlutterViewController {
|
|||||||
|
|
||||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||||
|
registerWirelessDataChannel(engineBridge)
|
||||||
let channel = FlutterMethodChannel(
|
let channel = FlutterMethodChannel(
|
||||||
name: "com.stormg.lumadrama/share",
|
name: "com.stormg.lumadrama/share",
|
||||||
binaryMessenger: engineBridge.applicationRegistrar.messenger()
|
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 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'app.dart';
|
import 'app.dart';
|
||||||
import 'core/config/app_config.dart';
|
import 'core/config/app_config.dart';
|
||||||
import 'core/config/server_endpoint.dart';
|
import 'core/config/server_endpoint.dart';
|
||||||
|
import 'core/platform/wireless_data_prompt.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await ServerEndpoint.load();
|
await ServerEndpoint.load();
|
||||||
AppConfig.ready = AppConfig.probeAndApplyBaseUrl();
|
AppConfig.ready = WirelessDataPrompt.awaitDecision(
|
||||||
|
enabled: !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS,
|
||||||
|
).then((_) => AppConfig.probeAndApplyBaseUrl());
|
||||||
runApp(const LumaDramaApp());
|
runApp(const LumaDramaApp());
|
||||||
unawaited(
|
unawaited(
|
||||||
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]),
|
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