diff --git a/app/lib/app_init.dart b/app/lib/app_init.dart index f1e2d74c..a533b7e0 100644 --- a/app/lib/app_init.dart +++ b/app/lib/app_init.dart @@ -74,7 +74,10 @@ Future init(InitIsolateType isolateType) async { if (features.isSupportSelfSignedCert) { await _initSelfSignedCertManager(); } - await initHttp(k.versionStr); + await initHttp( + appVersion: k.versionStr, + isNewHttpEngine: Pref().isNewHttpEngine() ?? false, + ); await _initDiContainer(isolateType); _initVisibilityDetector(); initGpsMap(); diff --git a/app/lib/controller/pref_controller.dart b/app/lib/controller/pref_controller.dart index e34a315f..3706530a 100644 --- a/app/lib/controller/pref_controller.dart +++ b/app/lib/controller/pref_controller.dart @@ -170,6 +170,12 @@ class PrefController { value: value, ); + Future setNewHttpEngine(bool value) => _set( + controller: _isNewHttpEngineController, + setter: (pref, value) => pref.setNewHttpEngine(value), + value: value, + ); + Future _set({ required BehaviorSubject controller, required Future Function(Pref pref, T value) setter, @@ -275,6 +281,9 @@ class PrefController { .getMapBrowserPrevPosition() ?.let(tryJsonDecode) ?.let(_tryMapCoordFromJson)); + @npSubjectAccessor + late final _isNewHttpEngineController = + BehaviorSubject.seeded(_c.pref.isNewHttpEngine() ?? false); } @npSubjectAccessor diff --git a/app/lib/controller/pref_controller.g.dart b/app/lib/controller/pref_controller.g.dart index 689ebad0..a907f513 100644 --- a/app/lib/controller/pref_controller.g.dart +++ b/app/lib/controller/pref_controller.g.dart @@ -166,6 +166,11 @@ extension $PrefControllerNpSubjectAccessor on PrefController { mapBrowserPrevPosition.distinct().skip(1); MapCoord? get mapBrowserPrevPositionValue => _mapBrowserPrevPositionController.value; +// _isNewHttpEngineController + ValueStream get isNewHttpEngine => _isNewHttpEngineController.stream; + Stream get isNewHttpEngineNew => isNewHttpEngine.skip(1); + Stream get isNewHttpEngineChange => isNewHttpEngine.distinct().skip(1); + bool get isNewHttpEngineValue => _isNewHttpEngineController.value; } extension $SecurePrefControllerNpSubjectAccessor on SecurePrefController { diff --git a/app/lib/controller/pref_controller/util.dart b/app/lib/controller/pref_controller/util.dart index 2ca3368d..7408a20b 100644 --- a/app/lib/controller/pref_controller/util.dart +++ b/app/lib/controller/pref_controller/util.dart @@ -98,6 +98,9 @@ extension on Pref { return provider.setString(PrefKey.mapBrowserPrevPosition, value); } } + + Future setNewHttpEngine(bool value) => + provider.setBool(PrefKey.isNewHttpEngine, value); } MapCoord? _tryMapCoordFromJson(dynamic json) { diff --git a/app/lib/entity/pref.dart b/app/lib/entity/pref.dart index 04f95dd1..f7def3b1 100644 --- a/app/lib/entity/pref.dart +++ b/app/lib/entity/pref.dart @@ -114,6 +114,7 @@ enum PrefKey implements PrefKeyInterface { protectedPageAuthPassword, dontShowVideoPreviewHint, mapBrowserPrevPosition, + isNewHttpEngine, ; @override @@ -202,6 +203,8 @@ enum PrefKey implements PrefKeyInterface { return "dontShowVideoPreviewHint"; case PrefKey.mapBrowserPrevPosition: return "mapBrowserPrevPosition"; + case PrefKey.isNewHttpEngine: + return "isNewHttpEngine"; } } } diff --git a/app/lib/entity/pref/extension.dart b/app/lib/entity/pref/extension.dart index 4898cc54..7c7f37f2 100644 --- a/app/lib/entity/pref/extension.dart +++ b/app/lib/entity/pref/extension.dart @@ -223,6 +223,8 @@ extension PrefExtension on Pref { PrefKey.isVideoPlayerLoop, value, (key, value) => provider.setBool(key, value)); + + bool? isNewHttpEngine() => provider.getBool(PrefKey.isNewHttpEngine); } extension AccountPrefExtension on AccountPref { diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index a7162afa..7884d944 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -454,6 +454,8 @@ "settingsClearCacheDatabaseDescription": "Clear cached file info and trigger a complete resync with the server", "settingsClearCacheDatabaseSuccessNotification": "Database cleared successfully. You are suggested to restart the app", "settingsManageTrustedCertificateTitle": "Manage trusted certificates", + "settingsUseNewHttpEngine": "Use new HTTP engine", + "settingsUseNewHttpEngineDescription": "New HTTP engine based on Chromium, supporting new standards like HTTP/2* and HTTP/3 QUIC*.\n\nLimitations:\nSelf-signed certs can no longer be managed by us. You must import your CA certs to the system trust store for them to work.\n\n* HTTPS is required for HTTP/2 and HTTP/3", "settingsAboutSectionTitle": "About", "@settingsAboutSectionTitle": { "description": "Title of the about section in settings widget" @@ -483,6 +485,7 @@ "@settingsTranslatorTitle": { "description": "Title of the translator item" }, + "settingsRestartNeededDialog": "Please restart app to apply change", "writePreferenceFailureNotification": "Failed setting preference", "@writePreferenceFailureNotification": { "description": "Inform user that the preference file cannot be modified" diff --git a/app/lib/l10n/untranslated-messages.txt b/app/lib/l10n/untranslated-messages.txt index bab42341..39cda39d 100644 --- a/app/lib/l10n/untranslated-messages.txt +++ b/app/lib/l10n/untranslated-messages.txt @@ -38,6 +38,8 @@ "settingsAppLockSetupPasswordDialogTitle", "settingsAppLockConfirmPasswordDialogTitle", "settingsManageTrustedCertificateTitle", + "settingsUseNewHttpEngine", + "settingsUseNewHttpEngineDescription", "settingsAboutSectionTitle", "settingsVersionTitle", "settingsServerVersionTitle", @@ -46,6 +48,7 @@ "settingsCaptureLogsTitle", "settingsCaptureLogsDescription", "settingsTranslatorTitle", + "settingsRestartNeededDialog", "writePreferenceFailureNotification", "enableButtonLabel", "exifSupportDetails", @@ -280,6 +283,9 @@ "settingsAppLockSetupPasswordDialogTitle", "settingsAppLockConfirmPasswordDialogTitle", "settingsManageTrustedCertificateTitle", + "settingsUseNewHttpEngine", + "settingsUseNewHttpEngineDescription", + "settingsRestartNeededDialog", "appLockUnlockHint", "appLockUnlockWrongPassword", "enabledText", @@ -309,6 +315,9 @@ "settingsThemePresets", "settingsAppLockSetupBiometricFallbackDialogTitle", "settingsManageTrustedCertificateTitle", + "settingsUseNewHttpEngine", + "settingsUseNewHttpEngineDescription", + "settingsRestartNeededDialog", "fileLastSharedByOthersDescription", "multipleFilesLinkShareDialogContent", "unshareLinkShareDirDialogContent", @@ -385,7 +394,10 @@ "settingsClearCacheDatabaseDescription", "settingsClearCacheDatabaseSuccessNotification", "settingsManageTrustedCertificateTitle", + "settingsUseNewHttpEngine", + "settingsUseNewHttpEngineDescription", "settingsServerVersionTitle", + "settingsRestartNeededDialog", "slideshowSetupDialogReverseTitle", "shareMethodPreviewTitle", "shareMethodPreviewDescription", @@ -497,6 +509,9 @@ "settingsAppLockSetupPasswordDialogTitle", "settingsAppLockConfirmPasswordDialogTitle", "settingsManageTrustedCertificateTitle", + "settingsUseNewHttpEngine", + "settingsUseNewHttpEngineDescription", + "settingsRestartNeededDialog", "appLockUnlockHint", "appLockUnlockWrongPassword", "enabledText", @@ -531,6 +546,9 @@ "settingsAppLockSetupPasswordDialogTitle", "settingsAppLockConfirmPasswordDialogTitle", "settingsManageTrustedCertificateTitle", + "settingsUseNewHttpEngine", + "settingsUseNewHttpEngineDescription", + "settingsRestartNeededDialog", "appLockUnlockHint", "appLockUnlockWrongPassword", "enabledText", @@ -565,6 +583,9 @@ "settingsAppLockSetupPasswordDialogTitle", "settingsAppLockConfirmPasswordDialogTitle", "settingsManageTrustedCertificateTitle", + "settingsUseNewHttpEngine", + "settingsUseNewHttpEngineDescription", + "settingsRestartNeededDialog", "appLockUnlockHint", "appLockUnlockWrongPassword", "enabledText", @@ -601,6 +622,9 @@ "settingsAppLockSetupPasswordDialogTitle", "settingsAppLockConfirmPasswordDialogTitle", "settingsManageTrustedCertificateTitle", + "settingsUseNewHttpEngine", + "settingsUseNewHttpEngineDescription", + "settingsRestartNeededDialog", "unmuteTooltip", "slideshowTooltip", "enhanceColorPopTitle", @@ -721,6 +745,8 @@ "settingsClearCacheDatabaseDescription", "settingsClearCacheDatabaseSuccessNotification", "settingsManageTrustedCertificateTitle", + "settingsUseNewHttpEngine", + "settingsUseNewHttpEngineDescription", "settingsAboutSectionTitle", "settingsVersionTitle", "settingsServerVersionTitle", @@ -729,6 +755,7 @@ "settingsCaptureLogsTitle", "settingsCaptureLogsDescription", "settingsTranslatorTitle", + "settingsRestartNeededDialog", "writePreferenceFailureNotification", "enableButtonLabel", "exifSupportDetails", @@ -1022,6 +1049,9 @@ "settingsAppLockSetupPasswordDialogTitle", "settingsAppLockConfirmPasswordDialogTitle", "settingsManageTrustedCertificateTitle", + "settingsUseNewHttpEngine", + "settingsUseNewHttpEngineDescription", + "settingsRestartNeededDialog", "enhanceColorPopTitle", "imageEditTransformOrientationClockwise", "imageEditTransformOrientationCounterclockwise", @@ -1064,7 +1094,10 @@ "settingsAppLockSetupPasswordDialogTitle", "settingsAppLockConfirmPasswordDialogTitle", "settingsManageTrustedCertificateTitle", + "settingsUseNewHttpEngine", + "settingsUseNewHttpEngineDescription", "settingsServerVersionTitle", + "settingsRestartNeededDialog", "searchLandingPeopleListEmptyText2", "createCollectionFailureNotification", "addItemToCollectionTooltip", @@ -1113,6 +1146,9 @@ "settingsAppLockSetupPasswordDialogTitle", "settingsAppLockConfirmPasswordDialogTitle", "settingsManageTrustedCertificateTitle", + "settingsUseNewHttpEngine", + "settingsUseNewHttpEngineDescription", + "settingsRestartNeededDialog", "appLockUnlockHint", "appLockUnlockWrongPassword", "enabledText", @@ -1133,6 +1169,9 @@ ], "tr": [ + "settingsUseNewHttpEngine", + "settingsUseNewHttpEngineDescription", + "settingsRestartNeededDialog", "mapBrowserDateRangeLabel", "mapBrowserDateRangeThisMonth", "mapBrowserDateRangePrevMonth", @@ -1163,6 +1202,9 @@ "settingsAppLockSetupPasswordDialogTitle", "settingsAppLockConfirmPasswordDialogTitle", "settingsManageTrustedCertificateTitle", + "settingsUseNewHttpEngine", + "settingsUseNewHttpEngineDescription", + "settingsRestartNeededDialog", "slideshowSetupDialogReverseTitle", "enhanceColorPopTitle", "enhanceRetouchTitle", @@ -1255,7 +1297,10 @@ "settingsClearCacheDatabaseDescription", "settingsClearCacheDatabaseSuccessNotification", "settingsManageTrustedCertificateTitle", + "settingsUseNewHttpEngine", + "settingsUseNewHttpEngineDescription", "settingsServerVersionTitle", + "settingsRestartNeededDialog", "sortOptionFilenameAscendingLabel", "sortOptionFilenameDescendingLabel", "slideshowSetupDialogReverseTitle", diff --git a/app/lib/widget/settings/expert/bloc.dart b/app/lib/widget/settings/expert/bloc.dart index 8831fa78..36be69a2 100644 --- a/app/lib/widget/settings/expert/bloc.dart +++ b/app/lib/widget/settings/expert/bloc.dart @@ -9,13 +9,19 @@ class _Error { } @npLog -class _Bloc extends Bloc<_Event, _State> with BlocLogger { +class _Bloc extends Bloc<_Event, _State> + with BlocLogger, BlocForEachMixin<_Event, _State> { _Bloc( DiContainer c, { required this.db, + required this.prefController, }) : _c = c, - super(const _State()) { + super(_State.init( + isNewHttpEngine: prefController.isNewHttpEngineValue, + )) { + on<_Init>(_onInit); on<_ClearCacheDatabase>(_onClearCacheDatabase); + on<_SetNewHttpEngine>(_onSetNewHttpEngine); } @override @@ -23,8 +29,18 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { Stream<_Error> errorStream() => _errorStream.stream; + Future _onInit(_Init ev, Emitter<_State> emit) async { + _log.info(ev); + return forEach( + emit, + prefController.isNewHttpEngineChange, + onData: (data) => state.copyWith(isNewHttpEngine: data), + ); + } + Future _onClearCacheDatabase( _ClearCacheDatabase ev, Emitter<_State> emit) async { + _log.info(ev); try { final accounts = _c.pref.getAccounts3Or([]); await db.clearAndInitWithAccounts(accounts.toDb()); @@ -35,7 +51,14 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { } } + void _onSetNewHttpEngine(_SetNewHttpEngine ev, Emitter<_State> emit) { + _log.info(ev); + prefController.setNewHttpEngine(ev.value); + } + final DiContainer _c; final NpDb db; + final PrefController prefController; + final _errorStream = StreamController<_Error>.broadcast(); } diff --git a/app/lib/widget/settings/expert/state_event.dart b/app/lib/widget/settings/expert/state_event.dart index 2b35d822..eb9f17fa 100644 --- a/app/lib/widget/settings/expert/state_event.dart +++ b/app/lib/widget/settings/expert/state_event.dart @@ -4,23 +4,50 @@ part of '../expert_settings.dart'; @toString class _State { const _State({ + required this.isNewHttpEngine, this.lastSuccessful, }); + factory _State.init({ + required bool isNewHttpEngine, + }) { + return _State( + isNewHttpEngine: isNewHttpEngine, + ); + } + @override String toString() => _$toString(); + final bool isNewHttpEngine; + final _Event? lastSuccessful; } -abstract class _Event { - const _Event(); -} +abstract class _Event {} @toString -class _ClearCacheDatabase extends _Event { - _ClearCacheDatabase(); +class _Init implements _Event { + const _Init(); @override String toString() => _$toString(); } + +@toString +class _ClearCacheDatabase implements _Event { + const _ClearCacheDatabase(); + + @override + String toString() => _$toString(); +} + +@toString +class _SetNewHttpEngine implements _Event { + const _SetNewHttpEngine(this.value); + + @override + String toString() => _$toString(); + + final bool value; +} diff --git a/app/lib/widget/settings/expert_settings.dart b/app/lib/widget/settings/expert_settings.dart index b4fedd9b..025d36e3 100644 --- a/app/lib/widget/settings/expert_settings.dart +++ b/app/lib/widget/settings/expert_settings.dart @@ -7,6 +7,7 @@ import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/bloc_util.dart'; +import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/db/entity_converter.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/pref.dart'; @@ -29,7 +30,8 @@ class ExpertSettings extends StatelessWidget { create: (_) => _Bloc( KiwiContainer().resolve(), db: context.read(), - ), + prefController: context.read(), + )..add(const _Init()), child: const _WrappedExpertSettings(), ); } @@ -64,29 +66,53 @@ class _WrappedExpertSettingsState extends State<_WrappedExpertSettings> { Widget build(BuildContext context) { return Scaffold( body: Builder( - builder: (context) => BlocListener<_Bloc, _State>( - listenWhen: (previous, current) => - !identical(previous.lastSuccessful, current.lastSuccessful), - listener: (context, state) { - if (state.lastSuccessful is _ClearCacheDatabase) { - showDialog( - context: context, - builder: (_) => AlertDialog( - content: Text(L10n.global() - .settingsClearCacheDatabaseSuccessNotification), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - MaterialLocalizations.of(context).closeButtonLabel), + builder: (context) => MultiBlocListener( + listeners: [ + _BlocListener( + listenWhen: (previous, current) => + !identical(previous.lastSuccessful, current.lastSuccessful), + listener: (context, state) { + if (state.lastSuccessful is _ClearCacheDatabase) { + showDialog( + context: context, + builder: (_) => AlertDialog( + content: Text(L10n.global() + .settingsClearCacheDatabaseSuccessNotification), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(MaterialLocalizations.of(context) + .closeButtonLabel), + ), + ], ), - ], - ), - ); - } - }, + ); + } + }, + ), + _BlocListenerT( + selector: (state) => state.isNewHttpEngine, + listener: (context, isNewHttpEngine) { + showDialog( + context: context, + builder: (context) => AlertDialog( + content: Text(L10n.global().settingsRestartNeededDialog), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + MaterialLocalizations.of(context).closeButtonLabel), + ), + ], + ), + ); + }, + ), + ], child: CustomScrollView( slivers: [ SliverAppBar( @@ -119,7 +145,7 @@ class _WrappedExpertSettingsState extends State<_WrappedExpertSettings> { subtitle: Text( L10n.global().settingsClearCacheDatabaseDescription), onTap: () { - context.read<_Bloc>().add(_ClearCacheDatabase()); + context.read<_Bloc>().add(const _ClearCacheDatabase()); }, ), ListTile( @@ -130,6 +156,27 @@ class _WrappedExpertSettingsState extends State<_WrappedExpertSettings> { .pushNamed(TrustedCertManager.routeName); }, ), + _BlocSelector( + selector: (state) => state.isNewHttpEngine, + builder: (context, isNewHttpEngine) => CheckboxListTile( + title: Text(L10n.global().settingsUseNewHttpEngine), + value: isNewHttpEngine, + onChanged: (value) async { + if (value == true) { + final result = await showDialog( + context: context, + builder: (context) => + const _NewHttpEngineDialog(), + ); + if (context.mounted && result == true) { + context.addEvent(const _SetNewHttpEngine(true)); + } + } else { + context.addEvent(const _SetNewHttpEngine(false)); + } + }, + ), + ), ], ), ), @@ -142,3 +189,40 @@ class _WrappedExpertSettingsState extends State<_WrappedExpertSettings> { late final StreamSubscription _errorSubscription; } + +class _NewHttpEngineDialog extends StatelessWidget { + const _NewHttpEngineDialog(); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(L10n.global().settingsUseNewHttpEngine), + content: Text(L10n.global().settingsUseNewHttpEngineDescription), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text(L10n.global().enableButtonLabel), + ), + ], + ); + } +} + +// typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; +typedef _BlocListener = BlocListener<_Bloc, _State>; +typedef _BlocListenerT = BlocListenerT<_Bloc, _State, T>; +typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; + +extension on BuildContext { + _Bloc get bloc => read<_Bloc>(); + // _State get state => bloc.state; + void addEvent(_Event event) => bloc.add(event); +} diff --git a/app/lib/widget/settings/expert_settings.g.dart b/app/lib/widget/settings/expert_settings.g.dart index 07f26cc9..292f5519 100644 --- a/app/lib/widget/settings/expert_settings.g.dart +++ b/app/lib/widget/settings/expert_settings.g.dart @@ -13,15 +13,17 @@ part of 'expert_settings.dart'; // ************************************************************************** abstract class $_StateCopyWithWorker { - _State call({_Event? lastSuccessful}); + _State call({bool? isNewHttpEngine, _Event? lastSuccessful}); } class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { _$_StateCopyWithWorkerImpl(this.that); @override - _State call({dynamic lastSuccessful = copyWithNull}) { + _State call( + {dynamic isNewHttpEngine, dynamic lastSuccessful = copyWithNull}) { return _State( + isNewHttpEngine: isNewHttpEngine as bool? ?? that.isNewHttpEngine, lastSuccessful: lastSuccessful == copyWithNull ? that.lastSuccessful : lastSuccessful as _Event?); @@ -61,7 +63,14 @@ extension _$_BlocNpLog on _Bloc { extension _$_StateToString on _State { String _$toString() { // ignore: unnecessary_string_interpolations - return "_State {lastSuccessful: $lastSuccessful}"; + return "_State {isNewHttpEngine: $isNewHttpEngine, lastSuccessful: $lastSuccessful}"; + } +} + +extension _$_InitToString on _Init { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_Init {}"; } } @@ -71,3 +80,10 @@ extension _$_ClearCacheDatabaseToString on _ClearCacheDatabase { return "_ClearCacheDatabase {}"; } } + +extension _$_SetNewHttpEngineToString on _SetNewHttpEngine { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetNewHttpEngine {value: $value}"; + } +} diff --git a/np_http/lib/src/http.dart b/np_http/lib/src/http.dart index 896b2434..9a52d1af 100644 --- a/np_http/lib/src/http.dart +++ b/np_http/lib/src/http.dart @@ -8,31 +8,39 @@ import 'http_stub.dart' if (dart.library.js_interop) 'http_browser.dart' if (dart.library.io) 'http_io.dart'; -Future initHttp(String appVersion) async { +Future initHttp({ + required String appVersion, + required bool isNewHttpEngine, +}) async { final userAgent = "nc-photos $appVersion"; Client? client; - if (getRawPlatform() == NpPlatform.android) { - try { - final cronetEngine = CronetEngine.build( - enableHttp2: true, - userAgent: userAgent, - ); - client = CronetClient.fromCronetEngine( - cronetEngine, - closeEngine: true, - ); - _log.info("Using cronet backend"); - } catch (e, stackTrace) { - _log.severe("Failed creating CronetEngine", e, stackTrace); - } - } else if (getRawPlatform().isApple) { - try { - final urlConfig = URLSessionConfiguration.ephemeralSessionConfiguration() - ..httpAdditionalHeaders = {"User-Agent": userAgent}; - client = CupertinoClient.fromSessionConfiguration(urlConfig); - _log.info("Using cupertino backend"); - } catch (e, stackTrace) { - _log.severe("Failed creating URLSessionConfiguration", e, stackTrace); + if (isNewHttpEngine) { + if (getRawPlatform() == NpPlatform.android) { + try { + final cronetEngine = CronetEngine.build( + enableHttp2: true, + enableQuic: true, + enableBrotli: true, + userAgent: userAgent, + ); + client = CronetClient.fromCronetEngine( + cronetEngine, + closeEngine: true, + ); + _log.info("Using cronet backend"); + } catch (e, stackTrace) { + _log.severe("Failed creating CronetEngine", e, stackTrace); + } + } else if (getRawPlatform().isApple) { + try { + final urlConfig = + URLSessionConfiguration.ephemeralSessionConfiguration() + ..httpAdditionalHeaders = {"User-Agent": userAgent}; + client = CupertinoClient.fromSessionConfiguration(urlConfig); + _log.info("Using cupertino backend"); + } catch (e, stackTrace) { + _log.severe("Failed creating URLSessionConfiguration", e, stackTrace); + } } } if (client == null) {