Compare commits

...

4 Commits

24 changed files with 384 additions and 273 deletions

14
.gitlint Normal file
View File

@ -0,0 +1,14 @@
[general]
ignore=B5,B6,B7,B8
[title-max-length]
line-length=72
[title-trailing-punctuation]
[title-hard-tab]
[title-match-regex]
regex=^(feat|fix|chore)\((android|ios|linux|windows|macos|all|docs|service|media|platform|notifications|picker|repo)\): [A-Z0-9].*$
[body-trailing-whitespace]
[body-first-line-empty]

View File

@ -10,9 +10,9 @@ import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.NonNull import androidx.annotation.NonNull
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.service.ServiceAware import io.flutter.embedding.engine.plugins.service.ServiceAware
import io.flutter.embedding.engine.plugins.service.ServicePluginBinding import io.flutter.embedding.engine.plugins.service.ServicePluginBinding
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
@ -99,7 +99,7 @@ class MoxxyNativePlugin : FlutterPlugin, ActivityAware, ServiceAware, BroadcastR
channel = MethodChannel(flutterPluginBinding.getBinaryMessenger(), SERVICE_FOREGROUND_METHOD_CHANNEL_KEY) channel = MethodChannel(flutterPluginBinding.getBinaryMessenger(), SERVICE_FOREGROUND_METHOD_CHANNEL_KEY)
LocalBroadcastManager.getInstance(context!!).registerReceiver( LocalBroadcastManager.getInstance(context!!).registerReceiver(
this, this,
IntentFilter(SERVICE_FOREGROUND_METHOD_CHANNEL_KEY) IntentFilter(SERVICE_FOREGROUND_METHOD_CHANNEL_KEY),
) )
// Register the picker handler // Register the picker handler

View File

@ -66,6 +66,13 @@ object BackgroundServiceStatic {
) )
} }
fun setConfiguration(context: Context, handle: Long, extraData: String) {
context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit()
.putLong(SERVICE_ENTRYPOINT_KEY, handle)
.putString(SERVICE_EXTRA_DATA_KEY, extraData)
.apply()
}
fun getStartAtBoot(context: Context): Boolean { fun getStartAtBoot(context: Context): Boolean {
return context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) return context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
.getBoolean( .getBoolean(
@ -87,6 +94,19 @@ object BackgroundServiceStatic {
false, false,
) )
} }
fun setManuallyStopped(context: Context, value: Boolean) {
context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit()
.putBoolean(SERVICE_MANUALLY_STOPPED_KEY, value)
.apply()
}
fun getHandle(context: Context): Long {
return context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getLong(
SERVICE_ENTRYPOINT_KEY,
0,
)
}
} }
class BackgroundService : Service(), MoxxyBackgroundServiceApi { class BackgroundService : Service(), MoxxyBackgroundServiceApi {
@ -110,19 +130,6 @@ class BackgroundService : Service(), MoxxyBackgroundServiceApi {
private var notificationTitle: String = SERVICE_DEFAULT_TITLE private var notificationTitle: String = SERVICE_DEFAULT_TITLE
private var notificationBody: String = SERVICE_DEFAULT_BODY private var notificationBody: String = SERVICE_DEFAULT_BODY
private fun setManuallyStopped(context: Context, value: Boolean) {
context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit()
.putBoolean(SERVICE_MANUALLY_STOPPED_KEY, value)
.apply()
}
private fun getHandle(): Long {
return getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getLong(
SERVICE_ENTRYPOINT_KEY,
0,
)
}
private fun updateNotificationInfo() { private fun updateNotificationInfo() {
val mutable = val mutable =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
@ -167,7 +174,7 @@ class BackgroundService : Service(), MoxxyBackgroundServiceApi {
null, null,
) )
val callback: FlutterCallbackInformation = val callback: FlutterCallbackInformation =
FlutterCallbackInformation.lookupCallbackInformation(getHandle()) FlutterCallbackInformation.lookupCallbackInformation(BackgroundServiceStatic.getHandle(this))
if (callback == null) { if (callback == null) {
Log.e(TAG, "Callback handle not found") Log.e(TAG, "Callback handle not found")
return return
@ -209,7 +216,7 @@ class BackgroundService : Service(), MoxxyBackgroundServiceApi {
if (!isManuallyStopped) { if (!isManuallyStopped) {
BackgroundServiceStatic.enqueue(this) BackgroundServiceStatic.enqueue(this)
} else { } else {
setManuallyStopped(applicationContext, true) BackgroundServiceStatic.setManuallyStopped(applicationContext, true)
} }
// Dispose of the engine // Dispose of the engine
@ -232,17 +239,13 @@ class BackgroundService : Service(), MoxxyBackgroundServiceApi {
} }
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
setManuallyStopped(this, false) BackgroundServiceStatic.setManuallyStopped(this, false)
BackgroundServiceStatic.enqueue(this) BackgroundServiceStatic.enqueue(this)
runService() runService()
return START_STICKY return START_STICKY
} }
override fun getHandler(): Long {
return getHandle()
}
override fun getExtraData(): String { override fun getExtraData(): String {
return getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getString( return getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getString(
SERVICE_EXTRA_DATA_KEY, SERVICE_EXTRA_DATA_KEY,

View File

@ -6,9 +6,6 @@ import android.content.Intent
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import org.moxxy.moxxy_native.MoxxyNativePlugin import org.moxxy.moxxy_native.MoxxyNativePlugin
import org.moxxy.moxxy_native.SERVICE_ENTRYPOINT_KEY
import org.moxxy.moxxy_native.SERVICE_EXTRA_DATA_KEY
import org.moxxy.moxxy_native.SERVICE_SHARED_PREFERENCES_KEY
import org.moxxy.moxxy_native.TAG import org.moxxy.moxxy_native.TAG
import org.moxxy.moxxy_native.service.BackgroundServiceStatic.setStartAtBoot import org.moxxy.moxxy_native.service.BackgroundServiceStatic.setStartAtBoot
@ -18,10 +15,11 @@ object PluginTracker {
class ServiceImplementation(private val context: Context) : MoxxyServiceApi { class ServiceImplementation(private val context: Context) : MoxxyServiceApi {
override fun configure(handle: Long, extraData: String) { override fun configure(handle: Long, extraData: String) {
context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit() BackgroundServiceStatic.setConfiguration(
.putLong(SERVICE_ENTRYPOINT_KEY, handle) context,
.putString(SERVICE_EXTRA_DATA_KEY, extraData) handle,
.apply() extraData,
)
} }
override fun isRunning(): Boolean { override fun isRunning(): Boolean {

View File

@ -43,7 +43,6 @@ class FlutterError(
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface MoxxyBackgroundServiceApi { interface MoxxyBackgroundServiceApi {
fun getHandler(): Long
fun getExtraData(): String fun getExtraData(): String
fun setNotificationBody(body: String) fun setNotificationBody(body: String)
fun sendData(data: String) fun sendData(data: String)
@ -58,22 +57,6 @@ interface MoxxyBackgroundServiceApi {
/** Sets up an instance of `MoxxyBackgroundServiceApi` to handle messages through the `binaryMessenger`. */ /** Sets up an instance of `MoxxyBackgroundServiceApi` to handle messages through the `binaryMessenger`. */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyBackgroundServiceApi?) { fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyBackgroundServiceApi?) {
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.getHandler", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
var wrapped: List<Any?>
try {
wrapped = listOf<Any?>(api.getHandler())
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run { run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.getExtraData", codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.getExtraData", codec)
if (api != null) { if (api != null) {

View File

@ -1,3 +1,4 @@
// ignore_for_file: avoid_print
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -17,6 +18,7 @@ Future<void> serviceHandleData(Map<String, dynamic>? data) async {
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<void> serviceEntrypoint(String initialLocale) async { Future<void> serviceEntrypoint(String initialLocale) async {
// avoid_print
print('Initial locale: $initialLocale'); print('Initial locale: $initialLocale');
} }
@ -61,7 +63,6 @@ class MyAppState extends State<MyApp> {
onPressed: () async { onPressed: () async {
final result = await MoxxyPickerApi() final result = await MoxxyPickerApi()
.pickFiles(FilePickerType.image, false); .pickFiles(FilePickerType.image, false);
// ignore: avoid_print
print('User picked: $result'); print('User picked: $result');
}, },
child: const Text('Photo picker'), child: const Text('Photo picker'),
@ -70,7 +71,6 @@ class MyAppState extends State<MyApp> {
onPressed: () async { onPressed: () async {
final result = await MoxxyPickerApi() final result = await MoxxyPickerApi()
.pickFiles(FilePickerType.imageAndVideo, true); .pickFiles(FilePickerType.imageAndVideo, true);
// ignore: avoid_print
print('User picked: $result'); print('User picked: $result');
}, },
child: const Text('Photo/Video multi-picker'), child: const Text('Photo/Video multi-picker'),
@ -79,7 +79,6 @@ class MyAppState extends State<MyApp> {
onPressed: () async { onPressed: () async {
final result = await MoxxyPickerApi() final result = await MoxxyPickerApi()
.pickFiles(FilePickerType.generic, true); .pickFiles(FilePickerType.generic, true);
// ignore: avoid_print
print('User picked: $result'); print('User picked: $result');
}, },
child: const Text('Generic multi-picker'), child: const Text('Generic multi-picker'),
@ -101,7 +100,6 @@ class MyAppState extends State<MyApp> {
'SHA-256', 'SHA-256',
); );
if (encResult == null) { if (encResult == null) {
// ignore: avoid_print
print('Failed to encrypt file'); print('Failed to encrypt file');
return; return;
} }
@ -115,7 +113,6 @@ class MyAppState extends State<MyApp> {
'SHA-256', 'SHA-256',
); );
if (decResult == null) { if (decResult == null) {
// ignore: avoid_print
print('Failed to decrypt file'); print('Failed to decrypt file');
return; return;
} }
@ -146,7 +143,7 @@ class MyAppState extends State<MyApp> {
await Permission.notification.request(); await Permission.notification.request();
final srv = ForegroundService(); final srv = getForegroundService();
await srv.start( await srv.start(
const ServiceConfig( const ServiceConfig(
serviceEntrypoint, serviceEntrypoint,
@ -159,7 +156,7 @@ class MyAppState extends State<MyApp> {
); );
await Future<void>.delayed(const Duration(milliseconds: 600)); await Future<void>.delayed(const Duration(milliseconds: 600));
await srv.dataSender.sendData( await getForegroundService().send(
TestCommand(), TestCommand(),
awaitable: false, awaitable: false,
); );

View File

@ -108,7 +108,7 @@ packages:
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
logging: logging:
dependency: transitive dependency: "direct main"
description: description:
name: logging name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"

View File

@ -30,6 +30,7 @@ dependencies:
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
permission_handler: ^10.4.5 permission_handler: ^10.4.5
get_it: ^7.6.0 get_it: ^7.6.0
logging: ^1.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -6,4 +6,7 @@ export 'pigeon/notifications.g.dart';
export 'pigeon/picker.g.dart'; export 'pigeon/picker.g.dart';
export 'pigeon/platform.g.dart'; export 'pigeon/platform.g.dart';
export 'pigeon/service.g.dart'; export 'pigeon/service.g.dart';
export 'src/service.dart'; export 'src/service/background/base.dart';
export 'src/service/config.dart';
export 'src/service/datasender/types.dart';
export 'src/service/foreground/base.dart';

View File

@ -18,33 +18,6 @@ class MoxxyBackgroundServiceApi {
static const MessageCodec<Object?> codec = StandardMessageCodec(); static const MessageCodec<Object?> codec = StandardMessageCodec();
Future<int> getHandler() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.getHandler', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(null) as List<Object?>?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
);
} else if (replyList.length > 1) {
throw PlatformException(
code: replyList[0]! as String,
message: replyList[1] as String?,
details: replyList[2],
);
} else if (replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (replyList[0] as int?)!;
}
}
Future<String> getExtraData() async { Future<String> getExtraData() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.getExtraData', codec, 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.getExtraData', codec,

View File

@ -22,8 +22,8 @@ class MoxxyServiceApi {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.configure', codec, 'dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.configure', codec,
binaryMessenger: _binaryMessenger); binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = final List<Object?>? replyList = await channel
await channel.send(<Object?>[arg_handle, arg_extraData]) as List<Object?>?; .send(<Object?>[arg_handle, arg_extraData]) as List<Object?>?;
if (replyList == null) { if (replyList == null) {
throw PlatformException( throw PlatformException(
code: 'channel-error', code: 'channel-error',
@ -44,8 +44,7 @@ class MoxxyServiceApi {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.isRunning', codec, 'dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.isRunning', codec,
binaryMessenger: _binaryMessenger); binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = final List<Object?>? replyList = await channel.send(null) as List<Object?>?;
await channel.send(null) as List<Object?>?;
if (replyList == null) { if (replyList == null) {
throw PlatformException( throw PlatformException(
code: 'channel-error', code: 'channel-error',
@ -71,8 +70,7 @@ class MoxxyServiceApi {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.start', codec, 'dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.start', codec,
binaryMessenger: _binaryMessenger); binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = final List<Object?>? replyList = await channel.send(null) as List<Object?>?;
await channel.send(null) as List<Object?>?;
if (replyList == null) { if (replyList == null) {
throw PlatformException( throw PlatformException(
code: 'channel-error', code: 'channel-error',

View File

@ -1,168 +0,0 @@
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<void> Function(String initialLocale);
typedef HandleEventCallback = Future<void> Function(Map<String, dynamic>? 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<String, dynamic>;
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<void> 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<String, dynamic>);
});
// Start execution
_log.finest('Setup complete. Calling main entrypoint...');
config.entrypoint(config.initialLocale);
}
void setNotificationBody(String body) {
_api.setNotificationBody(body);
}
}
class ForegroundServiceDataSender extends AwaitableDataSender<BackgroundCommand, BackgroundEvent> {
ForegroundServiceDataSender(this._api);
final MoxxyServiceApi _api;
@override
Future<void> sendDataImpl(DataWrapper<JsonImplementation> 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<void> attach(
HandleEventCallback handleData,
) async {
_channel.setMethodCallHandler((call) async {
await handleData(
jsonDecode(call.arguments! as String) as Map<String, dynamic>,
);
});
}
Future<void> 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<bool> isRunning() async {
WidgetsFlutterBinding.ensureInitialized();
return _api.isRunning();
}
}

View File

@ -0,0 +1,16 @@
import 'package:moxxy_native/src/service/config.dart';
import 'package:moxxy_native/src/service/datasender/types.dart';
/// Wrapper API that is only available to the background service.
abstract class BackgroundService {
/// Send [event] with optional id [id] to the foreground.
Future<void> send(BackgroundEvent event, {String? id});
/// Platform specific initialization routine that is called after
/// the entrypoint has been called.
Future<void> init(ServiceConfig config);
/// Update the notification body, if the platform shows a persistent
/// notification.
void setNotificationBody(String body);
}

View File

@ -0,0 +1,58 @@
import 'dart:convert';
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxy_native/pigeon/background_service.g.dart';
import 'package:moxxy_native/src/service/background/base.dart';
import 'package:moxxy_native/src/service/config.dart';
import 'package:moxxy_native/src/service/datasender/types.dart';
import 'package:uuid/uuid.dart';
class PigeonBackgroundService extends BackgroundService {
final MoxxyBackgroundServiceApi _api = MoxxyBackgroundServiceApi();
/// A method channel for Foreground -> Service communication
// TODO(Unknown): Move this into a constant for reuse
final MethodChannel _channel =
const MethodChannel('org.moxxy.moxxy_native/background');
/// A logger.
final Logger _log = Logger('PigeonBackgroundService');
@override
Future<void> send(BackgroundEvent event, {String? id}) async {
final data = DataWrapper(
id ?? const Uuid().v4(),
event,
);
await _api.sendData(jsonEncode(data.toJson()));
}
@override
Future<void> init(
ServiceConfig config,
) async {
// 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<String, dynamic>);
});
// Start execution
_log.finest('Setup complete. Calling main entrypoint...');
await config.entrypoint(config.initialLocale);
}
@override
void setNotificationBody(String body) {
_api.setNotificationBody(body);
}
}

View File

@ -0,0 +1,55 @@
import 'dart:convert';
import 'dart:ui';
/// A function that can act as a service entrypoint.
typedef EntrypointCallback = Future<void> Function(String initialLocale);
/// A function that can be called when data is received.
typedef HandleEventCallback = Future<void> Function(Map<String, dynamic>? data);
/// Configuration that will be passed to the service's entrypoint
class ServiceConfig {
const ServiceConfig(
this.entrypoint,
this.handleData,
this.initialLocale,
);
/// Reconstruct the configuration from a JSON string.
factory ServiceConfig.fromString(String rawData) {
final data = jsonDecode(rawData) as Map<String, dynamic>;
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,
);
}
/// The initial locale to use.
final String initialLocale;
/// The entrypoint to call into.
final EntrypointCallback entrypoint;
/// Entry function to call when the service receives data.
final HandleEventCallback handleData;
@override
String toString() {
return jsonEncode({
'entrypoint':
PluginUtilities.getCallbackHandle(entrypoint)!.toRawHandle(),
'handleData':
PluginUtilities.getCallbackHandle(handleData)!.toRawHandle(),
'initialLocale': initialLocale,
});
}
}

View File

@ -0,0 +1,15 @@
import 'dart:convert';
import 'package:moxlib/moxlib.dart';
import 'package:moxxy_native/pigeon/service.g.dart';
import 'package:moxxy_native/src/service/datasender/types.dart';
class PigeonForegroundServiceDataSender
extends AwaitableDataSender<BackgroundCommand, BackgroundEvent> {
PigeonForegroundServiceDataSender(this._api);
final MoxxyServiceApi _api;
@override
Future<void> sendDataImpl(DataWrapper<JsonImplementation> data) {
return _api.sendData(jsonEncode(data.toJson()));
}
}

View File

@ -0,0 +1,20 @@
import 'dart:io';
import 'package:moxlib/moxlib.dart';
import 'package:moxxy_native/pigeon/service.g.dart';
import 'package:moxxy_native/src/service/datasender/pigeon.dart';
import 'package:moxxy_native/src/service/exceptions.dart';
typedef ForegroundServiceDataSender
= AwaitableDataSender<BackgroundCommand, BackgroundEvent>;
abstract class BackgroundCommand implements JsonImplementation {}
abstract class BackgroundEvent implements JsonImplementation {}
ForegroundServiceDataSender getForegroundDataSender(MoxxyServiceApi api) {
if (Platform.isAndroid) {
return PigeonForegroundServiceDataSender(api);
} else {
throw UnsupportedPlatformException();
}
}

View File

@ -1,10 +1,14 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:moxxy_native/pigeon/background_service.g.dart'; import 'package:moxxy_native/pigeon/background_service.g.dart';
import 'package:moxxy_native/src/service.dart'; import 'package:moxxy_native/src/service/background/base.dart';
import 'package:moxxy_native/src/service/background/pigeon.dart';
import 'package:moxxy_native/src/service/config.dart';
/// An entrypoint that should be used when the service runs
/// in a new Flutter Engine.
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<void> androidEntrypoint() async { Future<void> pigeonEntrypoint() async {
// ignore: avoid_print // ignore: avoid_print
print('androidEntrypoint: Called on new FlutterEngine'); print('androidEntrypoint: Called on new FlutterEngine');
@ -15,7 +19,7 @@ Future<void> androidEntrypoint() async {
); );
// Setup the background service // Setup the background service
final srv = BackgroundService(); final srv = PigeonBackgroundService();
GetIt.I.registerSingleton(srv); GetIt.I.registerSingleton<BackgroundService>(srv);
srv.init(config); await srv.init(config);
} }

View File

@ -0,0 +1,8 @@
import 'dart:io';
/// An exception representing that moxxy_native does not support the given platform.
class UnsupportedPlatformException implements Exception {
UnsupportedPlatformException();
String get message => 'Unsupported platform "${Platform.operatingSystem}"';
}

View File

@ -0,0 +1,49 @@
import 'dart:io';
import 'package:moxlib/moxlib.dart';
import 'package:moxxy_native/src/service/config.dart';
import 'package:moxxy_native/src/service/datasender/types.dart';
import 'package:moxxy_native/src/service/exceptions.dart';
import 'package:moxxy_native/src/service/foreground/pigeon.dart';
/// Wrapper API that is only available to the UI isolate.
// TODO(Unknown): Dumb naming. Name it something better
abstract class ForegroundService {
/// Perform setup such that we [handleData] is called whenever the background service
/// sends data to the foreground.
Future<void> attach(HandleEventCallback handleData);
/// Start the background service with the config [config]. Additionally, perform
/// setup such that [uiHandleData] is called whenever the background service sends
/// data to the foreground.
Future<void> start(ServiceConfig config, HandleEventCallback uiHandleData);
/// Return true if the background service is running. False, if not.
Future<bool> isRunning();
/// Return the [AwaitableDataSender] that is used to send data to the background service.
ForegroundServiceDataSender getDataSender();
/// Convenience wrapper around getDataSender().sendData. The arguments are the same
/// as for [AwaitableDataSender].
Future<BackgroundEvent?> send(
BackgroundCommand command, {
bool awaitable = true,
});
}
/// "Singleton" ForegroundService instance to prevent having to type "GetIt.I.get<ForegroundService>()"
ForegroundService? _service;
/// Either returns or creates a [ForegroundService] object of the correct type for the
/// current platform.
ForegroundService getForegroundService() {
if (_service == null) {
if (Platform.isAndroid) {
_service = PigeonForegroundService();
} else {
throw UnsupportedPlatformException();
}
}
return _service!;
}

View File

@ -0,0 +1,82 @@
import 'dart:convert';
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:moxxy_native/pigeon/service.g.dart';
import 'package:moxxy_native/src/service/config.dart';
import 'package:moxxy_native/src/service/datasender/pigeon.dart';
import 'package:moxxy_native/src/service/datasender/types.dart';
import 'package:moxxy_native/src/service/entrypoints/pigeon.dart';
import 'package:moxxy_native/src/service/foreground/base.dart';
class PigeonForegroundService extends ForegroundService {
PigeonForegroundService() {
_dataSender = PigeonForegroundServiceDataSender(_api);
}
/// Pigeon channel to the native side.
final MoxxyServiceApi _api = MoxxyServiceApi();
/// A method channel for background service -> UI isolate communication.
final MethodChannel _channel =
const MethodChannel('org.moxxy.moxxy_native/foreground');
/// The data sender backing this class.
late final PigeonForegroundServiceDataSender _dataSender;
/// A logger.
final Logger _log = Logger('PigeonForegroundService');
@override
Future<void> attach(
HandleEventCallback handleData,
) async {
_channel.setMethodCallHandler((call) async {
await handleData(
jsonDecode(call.arguments! as String) as Map<String, dynamic>,
);
});
}
@override
Future<void> start(
ServiceConfig config,
HandleEventCallback uiHandleData,
) async {
await _api.configure(
PluginUtilities.getCallbackHandle(
pigeonEntrypoint,
)!
.toRawHandle(),
config.toString(),
);
// Prepare the method channel
await attach(uiHandleData);
// Start the service
await _api.start();
_log.finest('Background service started...');
}
@override
Future<bool> isRunning() async {
WidgetsFlutterBinding.ensureInitialized();
return _api.isRunning();
}
@override
ForegroundServiceDataSender getDataSender() => _dataSender;
@override
Future<BackgroundEvent?> send(
BackgroundCommand command, {
bool awaitable = true,
}) {
return _dataSender.sendData(
command,
awaitable: awaitable,
);
}
}

View File

@ -10,11 +10,8 @@ import 'package:pigeon/pigeon.dart';
), ),
), ),
) )
@HostApi() @HostApi()
abstract class MoxxyBackgroundServiceApi { abstract class MoxxyBackgroundServiceApi {
int getHandler();
String getExtraData(); String getExtraData();
void setNotificationBody(String body); void setNotificationBody(String body);

View File

@ -10,7 +10,6 @@ import 'package:pigeon/pigeon.dart';
), ),
), ),
) )
@HostApi() @HostApi()
abstract class MoxxyServiceApi { abstract class MoxxyServiceApi {
void configure(int handle, String extraData); void configure(int handle, String extraData);

6
scripts/lint.sh Normal file
View File

@ -0,0 +1,6 @@
# Format and lint the Dart code
dart format .
flutter analyze
# Format and lint the Kotlin code
ktlint --disabled_rules=standard:package-name --format android/src/main/kotlin/org/moxxy/moxxy_native