diff --git a/android/build.gradle b/android/build.gradle index 92b3a865e..4c0f175d1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -52,7 +52,7 @@ android { } dependencies { - api 'com.instabug.library:instabug:15.0.2.7160278-SNAPSHOT' + api 'com.instabug.library:instabug:16.0.0.6893269-SNAPSHOT' testImplementation 'junit:junit:4.13.2' testImplementation "org.mockito:mockito-inline:3.12.1" testImplementation "io.mockk:mockk:1.13.13" diff --git a/android/src/main/java/com/instabug/flutter/InstabugFlutterPlugin.java b/android/src/main/java/com/instabug/flutter/InstabugFlutterPlugin.java index 3a51a4051..2488e8147 100644 --- a/android/src/main/java/com/instabug/flutter/InstabugFlutterPlugin.java +++ b/android/src/main/java/com/instabug/flutter/InstabugFlutterPlugin.java @@ -11,7 +11,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleEventObserver; +import androidx.lifecycle.LifecycleOwner; +import com.instabug.flutter.generated.InstabugPigeon; import com.instabug.flutter.modules.ApmApi; import com.instabug.flutter.modules.BugReportingApi; import com.instabug.flutter.modules.CrashReportingApi; @@ -27,19 +31,23 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference; import io.flutter.embedding.engine.renderer.FlutterRenderer; import io.flutter.plugin.common.BinaryMessenger; -public class InstabugFlutterPlugin implements FlutterPlugin, ActivityAware { +public class InstabugFlutterPlugin implements FlutterPlugin, ActivityAware, LifecycleEventObserver { private static final String TAG = InstabugFlutterPlugin.class.getName(); @SuppressLint("StaticFieldLeak") private static Activity activity; + private InstabugPigeon.InstabugFlutterApi instabugFlutterApi; + private Lifecycle lifecycle; @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { register(binding.getApplicationContext(), binding.getBinaryMessenger(), (FlutterRenderer) binding.getTextureRegistry()); + instabugFlutterApi = new InstabugPigeon.InstabugFlutterApi(binding.getBinaryMessenger()); } @Override @@ -50,23 +58,60 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { @Override public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { activity = binding.getActivity(); + + // Register lifecycle observer if available + if (binding.getLifecycle() instanceof HiddenLifecycleReference) { + lifecycle = ((HiddenLifecycleReference) binding.getLifecycle()).getLifecycle(); + lifecycle.addObserver(this); + } } @Override public void onDetachedFromActivityForConfigChanges() { + if (lifecycle != null) { + lifecycle.removeObserver(this); + } activity = null; } @Override public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { activity = binding.getActivity(); + + // Re-register lifecycle observer if available + if (binding.getLifecycle() instanceof HiddenLifecycleReference) { + lifecycle = ((HiddenLifecycleReference) binding.getLifecycle()).getLifecycle(); + lifecycle.addObserver(this); + } } @Override public void onDetachedFromActivity() { + if (lifecycle != null) { + lifecycle.removeObserver(this); + lifecycle = null; + } activity = null; } + @Override + public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) { + if (event == Lifecycle.Event.ON_PAUSE) { + handleOnPause(); + } + } + + private void handleOnPause() { + if (instabugFlutterApi != null) { + instabugFlutterApi.dispose(new InstabugPigeon.InstabugFlutterApi.Reply() { + @Override + public void reply(Void reply) { + Log.d(TAG, "Screen render cleanup dispose called successfully"); + } + }); + } + } + private static void register(Context context, BinaryMessenger messenger, FlutterRenderer renderer) { final Callable screenshotProvider = new Callable() { @Override @@ -77,7 +122,7 @@ public Bitmap call() { Callable refreshRateProvider = new Callable() { @Override - public Float call(){ + public Float call() { return getRefreshRate(); } }; diff --git a/android/src/main/java/com/instabug/flutter/modules/ApmApi.java b/android/src/main/java/com/instabug/flutter/modules/ApmApi.java index fb58834e6..23b11b3fe 100644 --- a/android/src/main/java/com/instabug/flutter/modules/ApmApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/ApmApi.java @@ -93,23 +93,6 @@ public void setAutoUITraceEnabled(@NonNull Boolean isEnabled) { } } - /** - * Starts an execution trace and handles the result - * using callbacks. - * - * @param id The `id` parameter is a non-null String that represents the identifier of the execution - * trace. - * @param name The `name` parameter in the `startExecutionTrace` method represents the name of the - * execution trace that will be started. It is used as a reference to identify the trace during - * execution monitoring. - * @param result The `result` parameter in the `startExecutionTrace` method is an instance of - * `ApmPigeon.Result`. This parameter is used to provide the result of the execution trace - * operation back to the caller. The `success` method of the `result` object is called with the - * - * @deprecated see {@link #startFlow} - */ - - /** * Starts an AppFlow with the specified name. *
diff --git a/example/ios/Podfile b/example/ios/Podfile index 1768d6c13..410f49b97 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -30,7 +30,7 @@ target 'Runner' do use_frameworks! use_modular_headers! - pod 'Instabug', :podspec => 'https://ios-releases.instabug.com/custom/faeture-screen_rendering-release/15.1.31/Instabug.podspec' + pod 'Instabug', :podspec => 'https://ios-releases.instabug.com/custom/faeture-screen_rendering-release/15.1.32/Instabug.podspec' flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 648043285..14c2a7b68 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,14 +1,14 @@ PODS: - Flutter (1.0.0) - - Instabug (15.1.31) + - Instabug (15.1.32) - instabug_flutter (14.3.0): - Flutter - - Instabug (= 15.1.31) + - Instabug (= 15.1.32) - OCMock (3.6) DEPENDENCIES: - Flutter (from `Flutter`) - - Instabug (from `https://ios-releases.instabug.com/custom/faeture-screen_rendering-release/15.1.31/Instabug.podspec`) + - Instabug (from `https://ios-releases.instabug.com/custom/faeture-screen_rendering-release/15.1.32/Instabug.podspec`) - instabug_flutter (from `.symlinks/plugins/instabug_flutter/ios`) - OCMock (= 3.6) @@ -20,16 +20,16 @@ EXTERNAL SOURCES: Flutter: :path: Flutter Instabug: - :podspec: https://ios-releases.instabug.com/custom/faeture-screen_rendering-release/15.1.31/Instabug.podspec + :podspec: https://ios-releases.instabug.com/custom/faeture-screen_rendering-release/15.1.32/Instabug.podspec instabug_flutter: :path: ".symlinks/plugins/instabug_flutter/ios" SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - Instabug: 447d3f5a9f1c83120235437e08c9a51aaa8f8605 - instabug_flutter: 65aa2dee3036a3c7c8feff8e898c9547239a891d + Instabug: ee379b2694fa1dd3951526e5a34782bac886102e + instabug_flutter: 33230b1cc57be3b343b4d30f6dfdd03f9bf43599 OCMock: 5ea90566be239f179ba766fd9fbae5885040b992 -PODFILE CHECKSUM: c5b98f57c27da87950775c360d20aaedd3216b8d +PODFILE CHECKSUM: 41b206566c390a4111f60619beb4e420eba98359 COCOAPODS: 1.15.2 diff --git a/example/lib/main.dart b/example/lib/main.dart index 6979ec702..5946f75c1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -47,7 +47,7 @@ void main() { appVariant: 'variant 1', ); APM.setScreenRenderingEnabled(true); - APM.setAutoUITraceEnabled(false); + // APM.setAutoUITraceEnabled(false); FlutterError.onError = (FlutterErrorDetails details) { Zone.current.handleUncaughtError(details.exception, details.stack!); }; diff --git a/ios/instabug_flutter.podspec b/ios/instabug_flutter.podspec index f216fdaf0..2cf55fcdd 100644 --- a/ios/instabug_flutter.podspec +++ b/ios/instabug_flutter.podspec @@ -17,6 +17,6 @@ Pod::Spec.new do |s| s.pod_target_xcconfig = { 'OTHER_LDFLAGS' => '-framework "Flutter" -framework "InstabugSDK"'} s.dependency 'Flutter' - s.dependency 'Instabug', '15.1.31' + s.dependency 'Instabug', '15.1.32' end diff --git a/lib/src/modules/apm.dart b/lib/src/modules/apm.dart index 1ce12d413..3d28a0379 100644 --- a/lib/src/modules/apm.dart +++ b/lib/src/modules/apm.dart @@ -1,7 +1,6 @@ // ignore_for_file: avoid_classes_with_only_static_members import 'dart:async'; -import 'dart:developer'; import 'package:flutter/widgets.dart' show WidgetBuilder; import 'package:instabug_flutter/src/generated/apm.api.g.dart'; @@ -141,7 +140,6 @@ class APM { static Future startUITrace(String name) async { final isScreenRenderingEnabled = await FlagsConfig.screenRendering.isEnabled(); - log("startUITrace: isScreenRenderEnabled: $isScreenRenderingEnabled"); await InstabugScreenRenderManager.I .checkForScreenRenderInitialization(isScreenRenderingEnabled); diff --git a/lib/src/modules/instabug.dart b/lib/src/modules/instabug.dart index b0073b42a..7dc40873a 100644 --- a/lib/src/modules/instabug.dart +++ b/lib/src/modules/instabug.dart @@ -22,6 +22,7 @@ import 'package:instabug_flutter/src/utils/feature_flags_manager.dart'; import 'package:instabug_flutter/src/utils/ibg_build_info.dart'; import 'package:instabug_flutter/src/utils/instabug_logger.dart'; import 'package:instabug_flutter/src/utils/screen_name_masker.dart'; +import 'package:instabug_flutter/src/utils/screen_rendering/instabug_widget_binding_observer.dart'; import 'package:meta/meta.dart'; enum InvocationEvent { @@ -136,6 +137,23 @@ enum CustomTextPlaceHolderKey { enum ReproStepsMode { enabled, disabled, enabledWithNoScreenshots } +/// Disposal manager for handling Android lifecycle events +class _InstabugDisposalManager implements InstabugFlutterApi { + _InstabugDisposalManager._(); + + static final _InstabugDisposalManager _instance = + _InstabugDisposalManager._(); + + static _InstabugDisposalManager get instance => _instance; + + @override + void dispose() { + // Call the InstabugWidgetsBindingObserver dispose method when Android onPause is triggered + // to overcome calling onActivityDestroy() from android side before sending the data to it. + InstabugWidgetsBindingObserver.dispose(); + } +} + class Instabug { static var _host = InstabugHostApi(); @@ -154,6 +172,8 @@ class Instabug { BugReporting.$setup(); Replies.$setup(); Surveys.$setup(); + // Set up InstabugFlutterApi for Android onDestroy disposal + InstabugFlutterApi.setup(_InstabugDisposalManager.instance); } /// @nodoc diff --git a/lib/src/utils/screen_rendering/instabug_widget_binding_observer.dart b/lib/src/utils/screen_rendering/instabug_widget_binding_observer.dart index d454309c1..a085cf011 100644 --- a/lib/src/utils/screen_rendering/instabug_widget_binding_observer.dart +++ b/lib/src/utils/screen_rendering/instabug_widget_binding_observer.dart @@ -1,7 +1,9 @@ import 'package:flutter/widgets.dart'; +import 'package:instabug_flutter/src/utils/ibg_build_info.dart'; import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_manager.dart'; import 'package:instabug_flutter/src/utils/screen_name_masker.dart'; import 'package:instabug_flutter/src/utils/screen_rendering/instabug_screen_render_manager.dart'; +import 'package:instabug_flutter/src/utils/ui_trace/flags_config.dart'; import 'package:meta/meta.dart'; class InstabugWidgetsBindingObserver extends WidgetsBindingObserver { @@ -19,41 +21,58 @@ class InstabugWidgetsBindingObserver extends WidgetsBindingObserver { /// Logging tag for debugging purposes. static const tag = "InstabugWidgetsBindingObserver"; + /// Disposes all screen render resources. static void dispose() { - // Always call dispose to ensure proper cleanup with tracking flags + //Save the screen rendering data for the active traces Auto|Custom. + InstabugScreenRenderManager.I.stopScreenRenderCollector(); + // The dispose method is safe to call multiple times due to state tracking InstabugScreenRenderManager.I.dispose(); } void _handleResumedState() { final lastUiTrace = ScreenLoadingManager.I.currentUiTrace; - if (lastUiTrace == null) { - return; - } + + if (lastUiTrace == null) return; + final maskedScreenName = ScreenNameMasker.I.mask(lastUiTrace.screenName); + ScreenLoadingManager.I .startUiTrace(maskedScreenName, lastUiTrace.screenName) - .then((uiTraceId) { - if (uiTraceId != null && - InstabugScreenRenderManager.I.screenRenderEnabled) { - //End any active ScreenRenderCollector before starting a new one (Safe garde condition). - InstabugScreenRenderManager.I.endScreenRenderCollector(); - - //Start new ScreenRenderCollector. - InstabugScreenRenderManager.I - .startScreenRenderCollectorForTraceId(uiTraceId); - } + .then((uiTraceId) async { + if (uiTraceId == null) return; + + final isScreenRenderEnabled = + await FlagsConfig.screenRendering.isEnabled(); + + if (!isScreenRenderEnabled) return; + + await InstabugScreenRenderManager.I + .checkForScreenRenderInitialization(isScreenRenderEnabled); + + //End any active ScreenRenderCollector before starting a new one (Safe garde condition). + InstabugScreenRenderManager.I.endScreenRenderCollector(); + + //Start new ScreenRenderCollector. + InstabugScreenRenderManager.I + .startScreenRenderCollectorForTraceId(uiTraceId); }); } void _handlePausedState() { - if (InstabugScreenRenderManager.I.screenRenderEnabled) { + // Only handles iOS platform because in android we use pigeon @FlutterApi(). + // To overcome the onActivityDestroy() before sending the data to the android side. + if (InstabugScreenRenderManager.I.screenRenderEnabled && + IBGBuildInfo.I.isIOS) { InstabugScreenRenderManager.I.stopScreenRenderCollector(); } } Future _handleDetachedState() async { - if (InstabugScreenRenderManager.I.screenRenderEnabled) { + // Only handles iOS platform because in android we use pigeon @FlutterApi(). + // To overcome the onActivityDestroy() before sending the data to the android side. + if (InstabugScreenRenderManager.I.screenRenderEnabled && + IBGBuildInfo.I.isIOS) { dispose(); } } diff --git a/pigeons/instabug.api.dart b/pigeons/instabug.api.dart index aa91aec11..eede56de7 100644 --- a/pigeons/instabug.api.dart +++ b/pigeons/instabug.api.dart @@ -9,6 +9,11 @@ abstract class FeatureFlagsFlutterApi { ); } +@FlutterApi() +abstract class InstabugFlutterApi { + void dispose(); +} + @HostApi() abstract class InstabugHostApi { void setEnabled(bool isEnabled); diff --git a/test/apm_test.dart b/test/apm_test.dart index 36a40ba27..7a9e78230 100644 --- a/test/apm_test.dart +++ b/test/apm_test.dart @@ -256,6 +256,20 @@ void main() { verify(mHost.setScreenRenderEnabled(isEnabled)).called(1); }); + test("[setScreenRenderEnabled] should call host method when enabled", + () async { + const isEnabled = true; + await APM.setScreenRenderingEnabled(isEnabled); + verify(mHost.setScreenRenderEnabled(isEnabled)).called(1); + }); + + test("[setScreenRenderEnabled] should call host method when disabled", + () async { + const isEnabled = false; + await APM.setScreenRenderingEnabled(isEnabled); + verify(mHost.setScreenRenderEnabled(isEnabled)).called(1); + }); + test( "[startUITrace] should start screen render collector with right params, if screen render feature is enabled", () async { diff --git a/test/instabug_test.dart b/test/instabug_test.dart index d87208202..5e5041a3c 100644 --- a/test/instabug_test.dart +++ b/test/instabug_test.dart @@ -472,4 +472,22 @@ void main() { mHost.setTheme(themeConfig.toMap()), ).called(1); }); + + group('Disposal Manager', () { + test( + 'InstabugFlutterApi dispose should call widget binding observer dispose', + () { + // Test that the FlutterApi setup and dispose functionality works + // This verifies that when Android calls dispose(), it properly cleans up resources + expect( + () { + // Get the InstabugFlutterApi setup that was configured in Instabug.$setup() + // The actual dispose call will be tested in integration tests + // Here we just verify the setup doesn't crash + Instabug.$setup(); + }, + returnsNormally, + ); + }); + }); } diff --git a/test/utils/screen_render/instabug_widget_binding_observer_test.dart b/test/utils/screen_render/instabug_widget_binding_observer_test.dart index d3117c7ca..e11852ac2 100644 --- a/test/utils/screen_render/instabug_widget_binding_observer_test.dart +++ b/test/utils/screen_render/instabug_widget_binding_observer_test.dart @@ -1,5 +1,8 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:instabug_flutter/instabug_flutter.dart' show APM; +import 'package:instabug_flutter/src/generated/apm.api.g.dart'; +import 'package:instabug_flutter/src/utils/ibg_build_info.dart'; import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_manager.dart'; import 'package:instabug_flutter/src/utils/screen_name_masker.dart'; import 'package:instabug_flutter/src/utils/screen_rendering/instabug_screen_render_manager.dart'; @@ -15,6 +18,8 @@ import 'instabug_widget_binding_observer_test.mocks.dart'; ScreenLoadingManager, ScreenNameMasker, UiTrace, + ApmHostApi, + IBGBuildInfo, ]) void main() { late MockInstabugScreenRenderManager mockRenderManager; @@ -37,6 +42,13 @@ void main() { }); group('InstabugWidgetsBindingObserver', () { + final mApmHost = MockApmHostApi(); + final mIBGBuildInfo = MockIBGBuildInfo(); + setUp(() { + APM.$setHostApi(mApmHost); + IBGBuildInfo.setInstance(mIBGBuildInfo); + }); + test('returns the singleton instance', () { final instance = InstabugWidgetsBindingObserver.instance; final shorthand = InstabugWidgetsBindingObserver.I; @@ -52,6 +64,8 @@ void main() { .thenAnswer((_) async => 123); when(mockRenderManager.screenRenderEnabled).thenReturn(true); + when(mApmHost.isScreenRenderEnabled()).thenAnswer((_) async => true); + InstabugWidgetsBindingObserver.I .didChangeAppLifecycleState(AppLifecycleState.resumed); @@ -64,8 +78,11 @@ void main() { .called(1); }); - test('handles AppLifecycleState.paused and stops render collector', () { + test( + 'handles AppLifecycleState.paused and stops render collector for iOS platform', + () { when(mockRenderManager.screenRenderEnabled).thenReturn(true); + when(mIBGBuildInfo.isIOS).thenReturn(true); InstabugWidgetsBindingObserver.I .didChangeAppLifecycleState(AppLifecycleState.paused);