diff --git a/example/lib/main.dart b/example/lib/main.dart index e1f2950..c7763e1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,29 +1,23 @@ import 'dart:io'; import 'dart:typed_data'; -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get_it/get_it.dart'; import 'package:moxxy_native/moxxy_native.dart'; import 'package:permission_handler/permission_handler.dart'; +@pragma('vm:entrypoint') +Future serviceHandleData(Map? data) async { + print('[BG] Received data $data'); + GetIt.I.get().send( + TestEvent(), + id: data!['id']! as String, + ); +} + @pragma('vm:entry-point') -Future entrypoint() async { - WidgetsFlutterBinding.ensureInitialized(); - - print('CALLED FROM NEW FLUTTERENGINE'); - final api = MoxxyBackgroundServiceApi(); - final extra = await api.getExtraData(); - print('EXTRA DATA: $extra'); - - MethodChannel('org.moxxy.moxxy_native/background').setMethodCallHandler((call) async { - print('[BG] Received ${call.method} with ${call.arguments}'); - }); - - print('Waiting...'); - await Future.delayed(const Duration(seconds: 5)); - - await api.sendData('Hello from the foreground service'); - print('Data sent'); +Future serviceEntrypoint(String initialLocale) async { + print('Initial locale: $initialLocale'); } void main() { @@ -37,6 +31,20 @@ class MyApp extends StatefulWidget { MyAppState createState() => MyAppState(); } +class TestCommand extends BackgroundCommand { + @override + Map toJson() => { + 'request': 'return_name', + }; +} + +class TestEvent extends BackgroundEvent { + @override + Map toJson() => { + 'name': 'Moxxy', + }; +} + class MyAppState extends State { String? imagePath; @@ -138,15 +146,23 @@ class MyAppState extends State { await Permission.notification.request(); - final handle = PluginUtilities.getCallbackHandle(entrypoint)! - .toRawHandle(); - final api = MoxxyServiceApi(); - await api.configure(handle, 'lol'); - MethodChannel("org.moxxy.moxxy_native/foreground").setMethodCallHandler((call) async { - print('[FG] Received ${call.method} with ${call.arguments}'); - await api.sendData('Hello from the foreground'); - }); - await api.start(); + final srv = ForegroundService(); + await srv.start( + const ServiceConfig( + serviceEntrypoint, + serviceHandleData, + 'en', + ), + (data) async { + print('[FG] Received data $data'); + }, + ); + + await Future.delayed(const Duration(milliseconds: 600)); + await srv.dataSender.sendData( + TestCommand(), + awaitable: false, + ); }, child: const Text('Start foreground service')), ], diff --git a/example/pubspec.lock b/example/pubspec.lock index 9d7235d..110eadd 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -75,6 +83,14 @@ packages: description: flutter source: sdk version: "0.0.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468" + url: "https://pub.dev" + source: hosted + version: "7.6.0" js: dependency: transitive description: @@ -91,6 +107,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" matcher: dependency: transitive description: @@ -115,6 +139,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.0" + moxlib: + dependency: transitive + description: + name: moxlib + sha256: "2a76a632d23ea73906964cee4463352995e40199036162217ea323a6c3846e73" + url: "https://git.polynom.me/api/packages/Moxxy/pub/" + source: hosted + version: "0.2.0" moxxy_native: dependency: "direct main" description: @@ -215,6 +247,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + url: "https://pub.dev" + source: hosted + version: "3.1.0" term_glyph: dependency: transitive description: @@ -231,6 +271,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.16" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" vector_math: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 3ec4c28..14fdde1 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 permission_handler: ^10.4.5 + get_it: ^7.6.0 dev_dependencies: flutter_test: diff --git a/lib/moxxy_native.dart b/lib/moxxy_native.dart index 1b8f344..afd3c9e 100644 --- a/lib/moxxy_native.dart +++ b/lib/moxxy_native.dart @@ -6,3 +6,4 @@ export 'pigeon/notifications.g.dart'; export 'pigeon/picker.g.dart'; export 'pigeon/platform.g.dart'; export 'pigeon/service.g.dart'; +export 'src/service.dart'; diff --git a/lib/src/service.dart b/lib/src/service.dart new file mode 100644 index 0000000..668fd71 --- /dev/null +++ b/lib/src/service.dart @@ -0,0 +1,168 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:logging/logging.dart'; +import 'package:moxlib/moxlib.dart'; +import 'package:moxxy_native/moxxy_native.dart'; +import 'package:moxxy_native/src/service_android.dart'; +import 'package:uuid/uuid.dart'; + +typedef EntrypointCallback = Future Function(String initialLocale); +typedef HandleEventCallback = Future Function(Map? data); + +abstract class BackgroundCommand implements JsonImplementation {} + +abstract class BackgroundEvent implements JsonImplementation {} + +class ServiceConfig { + const ServiceConfig( + this.entrypoint, + this.handleData, + this.initialLocale, + ); + + factory ServiceConfig.fromString(String rawData) { + final data = jsonDecode(rawData) as Map; + return ServiceConfig( + PluginUtilities.getCallbackFromHandle( + CallbackHandle.fromRawHandle( + data['entrypoint']! as int, + ), + )! as EntrypointCallback, + PluginUtilities.getCallbackFromHandle( + CallbackHandle.fromRawHandle( + data['handleData']! as int, + ), + )! as HandleEventCallback, + data['initialLocale']! as String, + ); + } + + final String initialLocale; + final EntrypointCallback entrypoint; + final HandleEventCallback handleData; + + @override + String toString() { + return jsonEncode({ + 'entrypoint': PluginUtilities.getCallbackHandle(entrypoint)!.toRawHandle(), + 'handleData': PluginUtilities.getCallbackHandle(handleData)!.toRawHandle(), + 'initialLocale': initialLocale, + }); + } +} + +/// Wrapper API that is only available to the background service. +class BackgroundService { + final MoxxyBackgroundServiceApi _api = MoxxyBackgroundServiceApi(); + + /// A method channel for Foreground -> Service communication + // TODO(Unknown): Move this into a constant for reuse + final MethodChannel _channel = MethodChannel('org.moxxy.moxxy_native/background'); + + /// A logger. + final Logger _log = Logger('BackgroundService'); + + Future send(BackgroundEvent event, {String? id}) async { + final data = DataWrapper( + id ?? const Uuid().v4(), + event, + ); + + await _api.sendData(jsonEncode(data.toJson())); + } + + void init( + ServiceConfig config, + ) { + // Ensure that the Dart executor is ready to use plugins + WidgetsFlutterBinding.ensureInitialized(); + DartPluginRegistrant.ensureInitialized(); + + // Register the channel for Foreground -> Service communication + _channel.setMethodCallHandler((call) async { + // TODO(Unknown): Maybe do something smarter like pigeon and use Lists instead of Maps + final args = call.arguments! as String; + await config.handleData(jsonDecode(args) as Map); + }); + + // Start execution + _log.finest('Setup complete. Calling main entrypoint...'); + config.entrypoint(config.initialLocale); + } + + void setNotificationBody(String body) { + _api.setNotificationBody(body); + } +} + +class ForegroundServiceDataSender extends AwaitableDataSender { + ForegroundServiceDataSender(this._api); + final MoxxyServiceApi _api; + + @override + Future sendDataImpl(DataWrapper data) { + return _api.sendData(jsonEncode(data.toJson())); + } +} + +/// Wrapper API that is only available to the UI isolate. +// TODO(Unknown): Dumb naming. Name it something better +class ForegroundService { + ForegroundService() { + dataSender = ForegroundServiceDataSender(_api); + } + + final MoxxyServiceApi _api = MoxxyServiceApi(); + + /// A method channel for background service -> UI isolate communication. + final MethodChannel _channel = MethodChannel('org.moxxy.moxxy_native/foreground'); + + late final ForegroundServiceDataSender dataSender; + + /// A logger. + final Logger _log = Logger('ForegroundService'); + + Future attach( + HandleEventCallback handleData, + ) async { + _channel.setMethodCallHandler((call) async { + await handleData( + jsonDecode(call.arguments! as String) as Map, + ); + }); + } + + Future start( + ServiceConfig config, HandleEventCallback uiHandleData, + ) async { + int platformEntrypointHandle; + if (Platform.isAndroid) { + platformEntrypointHandle = PluginUtilities.getCallbackHandle( + androidEntrypoint, + )!.toRawHandle(); + } else { + // TODO: Custom exception + throw Exception('Unsupported platform'); + } + + // Configure the service on the native side + await _api.configure(platformEntrypointHandle, config.toString()); + + // Prepare the method channel + await attach(uiHandleData); + + // Start the service + await _api.start(); + _log.finest('Background service started...'); + } + + /// Returns true if the background service is already running. False, if not. + Future isRunning() async { + WidgetsFlutterBinding.ensureInitialized(); + return _api.isRunning(); + } +} diff --git a/lib/src/service_android.dart b/lib/src/service_android.dart new file mode 100644 index 0000000..231455a --- /dev/null +++ b/lib/src/service_android.dart @@ -0,0 +1,21 @@ +import 'package:flutter/cupertino.dart'; +import 'package:get_it/get_it.dart'; +import 'package:moxxy_native/pigeon/background_service.g.dart'; +import 'package:moxxy_native/src/service.dart'; + +@pragma('vm:entry-point') +Future androidEntrypoint() async { + // ignore: avoid_print + print('androidEntrypoint: Called on new FlutterEngine'); + + // Pull and deserialize the extra data passed on. + WidgetsFlutterBinding.ensureInitialized(); + final config = ServiceConfig.fromString( + await MoxxyBackgroundServiceApi().getExtraData(), + ); + + // Setup the background service + final srv = BackgroundService(); + GetIt.I.registerSingleton(srv); + srv.init(config); +} diff --git a/pubspec.yaml b/pubspec.yaml index 13378dc..72da4e6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,12 @@ environment: dependencies: flutter: sdk: flutter + get_it: ^7.6.0 + logging: ^1.2.0 + moxlib: + hosted: https://git.polynom.me/api/packages/Moxxy/pub + version: ^0.2.0 + uuid: ^3.0.5 dev_dependencies: flutter_lints: ^2.0.0