diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6da6fc6..153df02 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -197,7 +197,7 @@ jobs: integration_test_ios: name: ios integration test if: ${{ !contains(github.event.pull_request.labels.*.name, 'ci:skip') }} - runs-on: macos-12 + runs-on: macos-13 timeout-minutes: 60 steps: - uses: actions/checkout@v1 @@ -205,9 +205,9 @@ jobs: with: flutter-version: '3.10.0' # Run the latest version cache: true - - uses: futureware-tech/simulator-action@v1 + - uses: futureware-tech/simulator-action@v2 with: - model: 'iPhone 13 Pro Max' + model: 'iPhone 14 Pro Max' - name: ios integration test run: | flutter packages get diff --git a/android/src/main/java/com/agora/iris_method_channel/IrisMethodChannelPlugin.java b/android/src/main/java/com/agora/iris_method_channel/IrisMethodChannelPlugin.java index ce291f6..165c453 100644 --- a/android/src/main/java/com/agora/iris_method_channel/IrisMethodChannelPlugin.java +++ b/android/src/main/java/com/agora/iris_method_channel/IrisMethodChannelPlugin.java @@ -24,15 +24,13 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBindin @Override public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { - if (call.method.equals("getPlatformVersion")) { - result.success("Android " + android.os.Build.VERSION.RELEASE); - } else { - result.notImplemented(); - } + result.notImplemented(); } @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + // Notify the dart side to call the `IrisMethodChannel.dispose` to clear the resources. + channel.invokeMethod("onDetachedFromEngine_fromPlatform", null); channel.setMethodCallHandler(null); } } diff --git a/lib/src/iris_method_channel.dart b/lib/src/iris_method_channel.dart index 17cc495..bdc66e2 100644 --- a/lib/src/iris_method_channel.dart +++ b/lib/src/iris_method_channel.dart @@ -6,7 +6,8 @@ import 'dart:typed_data'; import 'package:async/async.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart' - show SynchronousFuture, VoidCallback, visibleForTesting; + show SynchronousFuture, VoidCallback, debugPrint, visibleForTesting; +import 'package:flutter/services.dart' show MethodChannel; import 'package:iris_method_channel/src/bindings/native_iris_api_common_bindings.dart' as iris; import 'package:iris_method_channel/src/iris_event.dart'; @@ -139,7 +140,7 @@ class _Messenger implements DisposableObject { @override Future dispose() async { - if (!_isDisposed) { + if (_isDisposed) { return; } _isDisposed = true; @@ -309,6 +310,8 @@ class IrisMethodChannel { late Isolate workerIsolate; late _HotRestartFinalizer _hotRestartFinalizer; + final MethodChannel _channel = const MethodChannel('iris_method_channel'); + static Future _execute(_InitilizationArgs args) async { final SendPort mainApiCallSendPort = args.apiCallPortSendPort; final SendPort mainEventSendPort = args.eventPortSendPort; @@ -401,6 +404,18 @@ class IrisMethodChannel { Isolate.exit(onExitSendPort, 0); } + void _setuponDetachedFromEngineListener() { + _channel.setMethodCallHandler((call) async { + if (call.method == 'onDetachedFromEngine_fromPlatform') { + debugPrint('Receive the onDetachedFromEngine callback, clean the native resources.'); + dispose(); + return true; + } + + return false; + }); + } + Future initilize(List args) async { if (_initilized) { return null; @@ -409,6 +424,8 @@ class IrisMethodChannel { final apiCallPort = ReceivePort(); final eventPort = ReceivePort(); + _setuponDetachedFromEngineListener(); + _hotRestartFinalizer = _HotRestartFinalizer(_nativeBindingsProvider); workerIsolate = await Isolate.spawn( diff --git a/test/iris_method_channel_test.dart b/test/iris_method_channel_test.dart index b9bc951..035cbc7 100644 --- a/test/iris_method_channel_test.dart +++ b/test/iris_method_channel_test.dart @@ -4,6 +4,8 @@ import 'dart:isolate'; import 'dart:typed_data'; import 'package:ffi/ffi.dart'; +import 'package:flutter/services.dart' show StandardMethodCodec, MethodCall; +import 'package:flutter_test/flutter_test.dart'; import 'package:iris_method_channel/src/bindings/native_iris_api_common_bindings.dart'; import 'package:iris_method_channel/src/bindings/native_iris_event_bindings.dart' as iris_event; @@ -11,7 +13,6 @@ import 'package:iris_method_channel/src/iris_event.dart'; import 'package:iris_method_channel/src/iris_method_channel.dart'; import 'package:iris_method_channel/src/native_bindings_delegate.dart'; import 'package:iris_method_channel/src/scoped_objects.dart'; -import 'package:test/test.dart'; class _ApiParam { _ApiParam(this.event, this.data); @@ -227,6 +228,8 @@ class _TestEventLoopEventHandler extends EventLoopEventHandler { } void main() { + final binding = TestWidgetsFlutterBinding.ensureInitialized(); + late _FakeNativeBindingDelegateMessenger messenger; late NativeBindingsProvider nativeBindingsProvider; late IrisMethodChannel irisMethodChannel; @@ -416,6 +419,61 @@ void main() { await irisMethodChannel.dispose(); }); + test('disposed', () async { + await irisMethodChannel.initilize([]); + await irisMethodChannel.dispose(); + // Wait for `dispose` completed. + await Future.delayed(const Duration(milliseconds: 500)); + final callRecord1 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'destroyNativeApiEngine'); + expect(callRecord1.length, 1); + + final callRecord2 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'destroyIrisEventHandler'); + expect(callRecord2.length, 1); + }); + + test('disposed multiple times', () async { + await irisMethodChannel.initilize([]); + await irisMethodChannel.dispose(); + await irisMethodChannel.dispose(); + // Wait for `dispose` completed. + await Future.delayed(const Duration(milliseconds: 500)); + final callRecord1 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'destroyNativeApiEngine'); + expect(callRecord1.length, 1); + + final callRecord2 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'destroyIrisEventHandler'); + expect(callRecord2.length, 1); + }); + + test('disposed after receive onDetachedFromEngine_fromPlatform', () async { + await irisMethodChannel.initilize([]); + + // Simulate the `MethodChannel` call from native side + const StandardMethodCodec codec = StandardMethodCodec(); + final ByteData data = codec.encodeMethodCall(const MethodCall( + 'onDetachedFromEngine_fromPlatform', + )); + await binding.defaultBinaryMessenger.handlePlatformMessage( + 'iris_method_channel', + data, + (ByteData? data) {}, + ); + + // Wait for the `iris_method_channel` method channel call completed. + await Future.delayed(const Duration(milliseconds: 1000)); + + final callRecord1 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'destroyNativeApiEngine'); + expect(callRecord1.length, 1); + + final callRecord2 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'destroyIrisEventHandler'); + expect(callRecord2.length, 1); + }); + test('invokeMethod after disposed', () async { await irisMethodChannel.initilize([]); await irisMethodChannel.dispose();