diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cb867f5c..fa026330e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [Unreleased](https://github.com/Instabug/Instabug-Flutter/compare/v15.0.2...dev) + +### Changed + +- **BREAKING** Remove deprecated APIs ([#614](https://github.com/Instabug/Instabug-Flutter/pull/614)). See migration guide for more details. +### Added + +- Add support for Advanced UI customization with comprehensive theming capabilities. ([#599](https://github.com/Instabug/Instabug-Flutter/pull/599)) + +- Add screen rendering monitoring functionality within the APM product. ([#605](https://github.com/Instabug/Instabug-Flutter/pull/605)) + + ## [15.0.2](https://github.com/Instabug/Instabug-Flutter/compare/v14.3.0...15.0.2) (Jul 7, 2025) ### Added diff --git a/android/build.gradle b/android/build.gradle index 3f5817b8c..9f27f88e1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -52,7 +52,7 @@ android { } dependencies { - api 'com.instabug.library:instabug:15.0.2.7020723-SNAPSHOT' + api 'com.instabug.library:instabug:15.0.2.7085294-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/modules/ApmApi.java b/android/src/main/java/com/instabug/flutter/modules/ApmApi.java index 61d0fca68..ab1f0dd86 100644 --- a/android/src/main/java/com/instabug/flutter/modules/ApmApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/ApmApi.java @@ -9,7 +9,7 @@ import com.instabug.apm.InternalAPM; import com.instabug.apm.configuration.cp.APMFeature; import com.instabug.apm.configuration.cp.FeatureAvailabilityCallback; -import com.instabug.apm.model.ExecutionTrace; +import com.instabug.apm.configuration.cp.ToleranceValueCallback; import com.instabug.apm.networking.APMNetworkLogger; import com.instabug.apm.networkinterception.cp.APMCPNetworkLog; import com.instabug.apm.screenrendering.models.cp.IBGFrameData; @@ -31,11 +31,10 @@ public class ApmApi implements ApmPigeon.ApmHostApi { private final String TAG = ApmApi.class.getName(); - private final HashMap traces = new HashMap<>(); - private final Callable refreshRate; + private final Callable refreshRateCallback; public ApmApi(Callable refreshRate) { - this.refreshRate = refreshRate; + this.refreshRateCallback = refreshRate; } public static void init(BinaryMessenger messenger, Callable refreshRateProvider) { @@ -94,57 +93,22 @@ 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} - */ - @Override - public void startExecutionTrace(@NonNull String id, @NonNull String name, ApmPigeon.Result result) { - ThreadManager.runOnBackground(new Runnable() { - @Override - public void run() { - try { - ExecutionTrace trace = APM.startExecutionTrace(name); - if (trace != null) { - traces.put(id, trace); - - ThreadManager.runOnMainThread(new Runnable() { - @Override - public void run() { - result.success(id); - } - }); - } else { - ThreadManager.runOnMainThread(new Runnable() { - @Override - public void run() { - result.success(null); - } - }); - } - } catch (Exception e) { - e.printStackTrace(); - - ThreadManager.runOnMainThread(new Runnable() { - @Override - public void run() { - result.success(null); - } - }); - } - } - }); - } + /** + * 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. @@ -209,37 +173,7 @@ public void endFlow(@NonNull String name) { } } - /** - * Adds a new attribute to trace - * - * @param id String id of the trace. - * @param key attribute key - * @param value attribute value. Null to remove attribute - * @deprecated see {@link #setFlowAttribute} - */ - @Override - public void setExecutionTraceAttribute(@NonNull String id, @NonNull String key, @NonNull String value) { - try { - traces.get(id).setAttribute(key, value); - } catch (Exception e) { - e.printStackTrace(); - } - } - /** - * Ends a trace - * - * @param id string id of the trace. - * @deprecated see {@link #endFlow} - */ - @Override - public void endExecutionTrace(@NonNull String id) { - try { - traces.get(id).end(); - } catch (Exception e) { - e.printStackTrace(); - } - } /** * Starts a UI trace. @@ -504,9 +438,16 @@ public void invoke(boolean isEnabled) { } @Override - public void deviceRefreshRate(@NonNull ApmPigeon.Result result) { + public void getDeviceRefreshRateAndTolerance(@NonNull ApmPigeon.Result> result) { try { - result.success(refreshRate.call().doubleValue()); + final double refreshRate = refreshRateCallback.call().doubleValue(); + InternalAPM._getToleranceValueForScreenRenderingCP(new ToleranceValueCallback() { + @Override + public void invoke(long tolerance) { + result.success(java.util.Arrays.asList(refreshRate, (double) tolerance)); + } + }); + } catch (Exception e) { e.printStackTrace(); } diff --git a/android/src/main/java/com/instabug/flutter/modules/BugReportingApi.java b/android/src/main/java/com/instabug/flutter/modules/BugReportingApi.java index f3236bb4e..c845de0c1 100644 --- a/android/src/main/java/com/instabug/flutter/modules/BugReportingApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/BugReportingApi.java @@ -184,7 +184,7 @@ public void setCommentMinimumCharacterCount(@NonNull Long limit, @Nullable List< reportTypesArray[i] = ArgsRegistry.reportTypes.get(key); } } - BugReporting.setCommentMinimumCharacterCount(limit.intValue(), reportTypesArray); + BugReporting.setCommentMinimumCharacterCountForBugReportType(limit.intValue(), reportTypesArray); } @Override diff --git a/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java b/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java index edfde055a..91782e85a 100644 --- a/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/InstabugApi.java @@ -5,6 +5,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; +import android.graphics.Typeface; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -176,7 +177,6 @@ public void setWelcomeMessageMode(@NonNull String mode) { @Override public void setPrimaryColor(@NonNull Long color) { - Instabug.setPrimaryColor(color.intValue()); } @Override @@ -228,20 +228,7 @@ public void run() { ); } - @Override - public void addExperiments(@NonNull List experiments) { - Instabug.addExperiments(experiments); - } - - @Override - public void removeExperiments(@NonNull List experiments) { - Instabug.removeExperiments(experiments); - } - @Override - public void clearAllExperiments() { - Instabug.clearAllExperiments(); - } @Override public void addFeatureFlags(@NonNull Map featureFlags) { @@ -509,4 +496,157 @@ public void setNetworkLogBodyEnabled(@NonNull Boolean isEnabled) { e.printStackTrace(); } } + + @Override + public void setTheme(@NonNull Map themeConfig) { + try { + Log.d(TAG, "setTheme called with config: " + themeConfig.toString()); + + com.instabug.library.model.IBGTheme.Builder builder = new com.instabug.library.model.IBGTheme.Builder(); + + if (themeConfig.containsKey("primaryColor")) { + builder.setPrimaryColor(getColor(themeConfig, "primaryColor")); + } + if (themeConfig.containsKey("secondaryTextColor")) { + builder.setSecondaryTextColor(getColor(themeConfig, "secondaryTextColor")); + } + if (themeConfig.containsKey("primaryTextColor")) { + builder.setPrimaryTextColor(getColor(themeConfig, "primaryTextColor")); + } + if (themeConfig.containsKey("titleTextColor")) { + builder.setTitleTextColor(getColor(themeConfig, "titleTextColor")); + } + if (themeConfig.containsKey("backgroundColor")) { + builder.setBackgroundColor(getColor(themeConfig, "backgroundColor")); + } + + if (themeConfig.containsKey("primaryTextStyle")) { + builder.setPrimaryTextStyle(getTextStyle(themeConfig, "primaryTextStyle")); + } + if (themeConfig.containsKey("secondaryTextStyle")) { + builder.setSecondaryTextStyle(getTextStyle(themeConfig, "secondaryTextStyle")); + } + if (themeConfig.containsKey("ctaTextStyle")) { + builder.setCtaTextStyle(getTextStyle(themeConfig, "ctaTextStyle")); + } + + setFontIfPresent(themeConfig, builder, "primaryFontPath", "primaryFontAsset", "primary"); + setFontIfPresent(themeConfig, builder, "secondaryFontPath", "secondaryFontAsset", "secondary"); + setFontIfPresent(themeConfig, builder, "ctaFontPath", "ctaFontAsset", "CTA"); + + com.instabug.library.model.IBGTheme theme = builder.build(); + Instabug.setTheme(theme); + Log.d(TAG, "Theme applied successfully"); + + } catch (Exception e) { + Log.e(TAG, "Error in setTheme: " + e.getMessage()); + e.printStackTrace(); + } + } + + + + /** + * Retrieves a color value from the Map. + * + * @param map The Map object. + * @param key The key to look for. + * @return The parsed color as an integer, or black if missing or invalid. + */ + private int getColor(Map map, String key) { + try { + if (map != null && map.containsKey(key) && map.get(key) != null) { + String colorString = (String) map.get(key); + return android.graphics.Color.parseColor(colorString); + } + } catch (Exception e) { + e.printStackTrace(); + } + return android.graphics.Color.BLACK; + } + + /** + * Retrieves a text style from the Map. + * + * @param map The Map object. + * @param key The key to look for. + * @return The corresponding Typeface style, or Typeface.NORMAL if missing or invalid. + */ + private int getTextStyle(Map map, String key) { + try { + if (map != null && map.containsKey(key) && map.get(key) != null) { + String style = (String) map.get(key); + switch (style.toLowerCase()) { + case "bold": + return Typeface.BOLD; + case "italic": + return Typeface.ITALIC; + case "bold_italic": + return Typeface.BOLD_ITALIC; + case "normal": + default: + return Typeface.NORMAL; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return Typeface.NORMAL; + } + + /** + * Sets a font on the theme builder if the font configuration is present in the theme config. + * + * @param themeConfig The theme configuration map + * @param builder The theme builder + * @param fileKey The key for font file path + * @param assetKey The key for font asset path + * @param fontType The type of font (for logging purposes) + */ + private void setFontIfPresent(Map themeConfig, com.instabug.library.model.IBGTheme.Builder builder, + String fileKey, String assetKey, String fontType) { + if (themeConfig.containsKey(fileKey) || themeConfig.containsKey(assetKey)) { + Typeface typeface = getTypeface(themeConfig, fileKey, assetKey); + if (typeface != null) { + switch (fontType) { + case "primary": + builder.setPrimaryTextFont(typeface); + break; + case "secondary": + builder.setSecondaryTextFont(typeface); + break; + case "CTA": + builder.setCtaTextFont(typeface); + break; + } + } + } + } + + private Typeface getTypeface(Map map, String fileKey, String assetKey) { + String fontName = null; + + if (assetKey != null && map.containsKey(assetKey) && map.get(assetKey) != null) { + fontName = (String) map.get(assetKey); + } else if (fileKey != null && map.containsKey(fileKey) && map.get(fileKey) != null) { + fontName = (String) map.get(fileKey); + } + + if (fontName == null) { + return Typeface.DEFAULT; + } + + try { + String assetPath = "fonts/" + fontName; + return Typeface.createFromAsset(context.getAssets(), assetPath); + } catch (Exception e) { + try { + return Typeface.create(fontName, Typeface.NORMAL); + } catch (Exception e2) { + return Typeface.DEFAULT; + } + } + } + + } diff --git a/android/src/test/java/com/instabug/flutter/ApmApiTest.java b/android/src/test/java/com/instabug/flutter/ApmApiTest.java index ab5ec93ab..d7bacc450 100644 --- a/android/src/test/java/com/instabug/flutter/ApmApiTest.java +++ b/android/src/test/java/com/instabug/flutter/ApmApiTest.java @@ -5,7 +5,6 @@ import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; @@ -16,7 +15,7 @@ import com.instabug.apm.InternalAPM; import com.instabug.apm.configuration.cp.APMFeature; import com.instabug.apm.configuration.cp.FeatureAvailabilityCallback; -import com.instabug.apm.model.ExecutionTrace; +import com.instabug.apm.configuration.cp.ToleranceValueCallback; import com.instabug.apm.networking.APMNetworkLogger; import com.instabug.flutter.generated.ApmPigeon; import com.instabug.flutter.modules.ApmApi; @@ -25,20 +24,18 @@ import org.json.JSONObject; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.MockedConstruction; import org.mockito.MockedStatic; +import java.lang.reflect.Array; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.Callable; -import static com.instabug.flutter.util.GlobalMocks.reflected; -import static com.instabug.flutter.util.MockResult.makeResult; -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import io.flutter.plugin.common.BinaryMessenger; @@ -68,16 +65,6 @@ public void cleanUp() { GlobalMocks.close(); } - private ExecutionTrace mockTrace(String id) { - String name = "trace-name"; - ExecutionTrace mTrace = mock(ExecutionTrace.class); - - mAPM.when(() -> APM.startExecutionTrace(name)).thenReturn(mTrace); - - api.startExecutionTrace(id, name, makeResult()); - - return mTrace; - } @Test public void testInit() { @@ -115,53 +102,7 @@ public void testSetAutoUITraceEnabled() { mAPM.verify(() -> APM.setAutoUITraceEnabled(isEnabled)); } - @Test - public void testStartExecutionTraceWhenTraceNotNull() { - String expectedId = "trace-id"; - String name = "trace-name"; - ApmPigeon.Result result = makeResult((String actualId) -> assertEquals(expectedId, actualId)); - - mAPM.when(() -> APM.startExecutionTrace(name)).thenReturn(new ExecutionTrace(name)); - - api.startExecutionTrace(expectedId, name, result); - - mAPM.verify(() -> APM.startExecutionTrace(name)); - } - - @Test - public void testStartExecutionTraceWhenTraceIsNull() { - String id = "trace-id"; - String name = "trace-name"; - ApmPigeon.Result result = makeResult(Assert::assertNull); - - mAPM.when(() -> APM.startExecutionTrace(name)).thenReturn(null); - - api.startExecutionTrace(id, name, result); - - mAPM.verify(() -> APM.startExecutionTrace(name)); - } - - @Test - public void testSetExecutionTraceAttribute() { - String id = "trace-id"; - String key = "is_premium"; - String value = "true"; - ExecutionTrace mTrace = mockTrace(id); - - api.setExecutionTraceAttribute(id, key, value); - - verify(mTrace).setAttribute(key, value); - } - - @Test - public void testEndExecutionTrace() { - String id = "trace-id"; - ExecutionTrace mTrace = mockTrace(id); - - api.endExecutionTrace(id); - verify(mTrace).end(); - } @Test public void testStartFlow() { @@ -418,24 +359,10 @@ public void testSetScreenRenderEnabled() { mAPM.verify(() -> APM.setScreenRenderingEnabled(isEnabled)); } - @Test - public void testDeviceRefreshRate() throws Exception { - float expectedRefreshRate = 60.0f; - Double expectedResult = 60.0; - ApmPigeon.Result result = spy(makeResult((actual) -> assertEquals(expectedResult, actual))); - - // Mock the refresh rate provider to return the expected value - Callable mockRefreshRateProvider = () -> expectedRefreshRate; - ApmApi testApi = new ApmApi(mockRefreshRateProvider); - - testApi.deviceRefreshRate(result); - - verify(result).success(expectedResult); - } @Test public void testDeviceRefreshRateWithException() throws Exception { - ApmPigeon.Result result = spy(makeResult((actual) -> {})); + ApmPigeon.Result> result = spy(makeResult((actual) -> {})); // Mock the refresh rate provider to throw an exception Callable mockRefreshRateProvider = () -> { @@ -443,13 +370,41 @@ public void testDeviceRefreshRateWithException() throws Exception { }; ApmApi testApi = new ApmApi(mockRefreshRateProvider); - testApi.deviceRefreshRate(result); + testApi.getDeviceRefreshRateAndTolerance(result); // Verify that the method doesn't crash when an exception occurs // The exception is caught and printed, but the result is not called verify(result, never()).success(any()); } + @Test + public void testGetDeviceRefreshRateAndTolerance() throws Exception { + // Arrange + double expectedRefreshRate = 60.0; + long expectedTolerance = 5L; + List expectedResult = Arrays.asList(expectedRefreshRate, (double) expectedTolerance); + ApmPigeon.Result> result = spy(makeResult((actual) -> assertEquals(expectedResult, actual))); + + // Mock the refresh rate provider + Callable mockRefreshRateProvider = () -> (float) expectedRefreshRate; + ApmApi testApi = new ApmApi(mockRefreshRateProvider); + + // Mock the tolerance callback + mInternalApmStatic.when(() -> InternalAPM._getToleranceValueForScreenRenderingCP(any(ToleranceValueCallback.class))).thenAnswer(invocation -> { + ToleranceValueCallback callback = invocation.getArgument(0); + callback.invoke(expectedTolerance); + return null; + }); + + // Act + testApi.getDeviceRefreshRateAndTolerance(result); + + // Assert + verify(result).success(expectedResult); + mInternalApmStatic.verify(() -> InternalAPM._getToleranceValueForScreenRenderingCP(any(ToleranceValueCallback.class))); + mInternalApmStatic.verifyNoMoreInteractions(); + } + @Test public void testEndScreenRenderForAutoUiTrace() { Map data = new HashMap<>(); diff --git a/android/src/test/java/com/instabug/flutter/BugReportingApiTest.java b/android/src/test/java/com/instabug/flutter/BugReportingApiTest.java index 6d22e26b8..50722762f 100644 --- a/android/src/test/java/com/instabug/flutter/BugReportingApiTest.java +++ b/android/src/test/java/com/instabug/flutter/BugReportingApiTest.java @@ -192,7 +192,7 @@ public void testSetCommentMinimumCharacterCount() { api.setCommentMinimumCharacterCount(limit, reportTypes); - mBugReporting.verify(() -> BugReporting.setCommentMinimumCharacterCount(limit.intValue(), BugReporting.ReportType.BUG, BugReporting.ReportType.QUESTION)); + mBugReporting.verify(() -> BugReporting.setCommentMinimumCharacterCountForBugReportType(limit.intValue(), BugReporting.ReportType.BUG, BugReporting.ReportType.QUESTION)); } @Test diff --git a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java index 97b9cdf7b..6be2eef92 100644 --- a/android/src/test/java/com/instabug/flutter/InstabugApiTest.java +++ b/android/src/test/java/com/instabug/flutter/InstabugApiTest.java @@ -80,6 +80,8 @@ import org.mockito.verification.VerificationMode; import org.mockito.verification.VerificationMode; +import android.graphics.Typeface; + public class InstabugApiTest { private final Callable screenshotProvider = () -> mock(Bitmap.class); private final Application mContext = mock(Application.class); @@ -276,13 +278,7 @@ public void testSetWelcomeMessageMode() { @Test public void testSetPrimaryColor() { - Long color = 0xFF0000L; - - api.setPrimaryColor(color); - - mInstabug.verify(() -> Instabug.setPrimaryColor(0xFF0000)); } - @Test public void testSetSessionProfilerEnabledGivenTrue() { Boolean isEnabled = true; @@ -346,30 +342,7 @@ public void testGetTags() { mInstabug.verify(Instabug::getTags); } - @Test - public void testAddExperiments() { - List experiments = Arrays.asList("premium", "star"); - api.addExperiments(experiments); - - mInstabug.verify(() -> Instabug.addExperiments(experiments)); - } - - @Test - public void testRemoveExperiments() { - List experiments = Arrays.asList("premium", "star"); - - api.removeExperiments(experiments); - - mInstabug.verify(() -> Instabug.removeExperiments(experiments)); - } - - @Test - public void testClearAllExperiments() { - api.clearAllExperiments(); - - mInstabug.verify(Instabug::clearAllExperiments); - } @Test public void testAddFeatureFlags() { @@ -658,4 +631,53 @@ public void testSetNetworkLogBodyDisabled() { mInstabug.verify(() -> Instabug.setNetworkLogBodyEnabled(false)); } + + @Test + public void testSetThemeWithAllProperties() { + Map themeConfig = new HashMap<>(); + themeConfig.put("primaryColor", "#FF6B6B"); + themeConfig.put("backgroundColor", "#FFFFFF"); + themeConfig.put("titleTextColor", "#000000"); + themeConfig.put("primaryTextColor", "#333333"); + themeConfig.put("secondaryTextColor", "#666666"); + themeConfig.put("primaryTextStyle", "bold"); + themeConfig.put("secondaryTextStyle", "italic"); + themeConfig.put("ctaTextStyle", "bold_italic"); + themeConfig.put("primaryFontAsset", "assets/fonts/CustomFont-Regular.ttf"); + themeConfig.put("secondaryFontAsset", "assets/fonts/CustomFont-Bold.ttf"); + themeConfig.put("ctaFontAsset", "assets/fonts/CustomFont-Italic.ttf"); + + MockedConstruction mThemeBuilder = + mockConstruction(com.instabug.library.model.IBGTheme.Builder.class, (mock, context) -> { + when(mock.setPrimaryColor(anyInt())).thenReturn(mock); + when(mock.setBackgroundColor(anyInt())).thenReturn(mock); + when(mock.setTitleTextColor(anyInt())).thenReturn(mock); + when(mock.setPrimaryTextColor(anyInt())).thenReturn(mock); + when(mock.setSecondaryTextColor(anyInt())).thenReturn(mock); + when(mock.setPrimaryTextStyle(anyInt())).thenReturn(mock); + when(mock.setSecondaryTextStyle(anyInt())).thenReturn(mock); + when(mock.setCtaTextStyle(anyInt())).thenReturn(mock); + when(mock.setPrimaryTextFont(any(Typeface.class))).thenReturn(mock); + when(mock.setSecondaryTextFont(any(Typeface.class))).thenReturn(mock); + when(mock.setCtaTextFont(any(Typeface.class))).thenReturn(mock); + when(mock.build()).thenReturn(mock(com.instabug.library.model.IBGTheme.class)); + }); + + api.setTheme(themeConfig); + + com.instabug.library.model.IBGTheme.Builder builder = mThemeBuilder.constructed().get(0); + + verify(builder).setPrimaryColor(anyInt()); + verify(builder).setBackgroundColor(anyInt()); + verify(builder).setTitleTextColor(anyInt()); + verify(builder).setPrimaryTextColor(anyInt()); + verify(builder).setSecondaryTextColor(anyInt()); + verify(builder).setPrimaryTextStyle(Typeface.BOLD); + verify(builder).setSecondaryTextStyle(Typeface.ITALIC); + verify(builder).setCtaTextStyle(Typeface.BOLD_ITALIC); + + mInstabug.verify(() -> Instabug.setTheme(any(com.instabug.library.model.IBGTheme.class))); + } + + } diff --git a/example/android/app/src/main/kotlin/com/example/InstabugSample/InstabugExampleMethodCallHandler.kt b/example/android/app/src/main/kotlin/com/example/InstabugSample/InstabugExampleMethodCallHandler.kt index 17a7d35c6..f6fb0fc03 100644 --- a/example/android/app/src/main/kotlin/com/example/InstabugSample/InstabugExampleMethodCallHandler.kt +++ b/example/android/app/src/main/kotlin/com/example/InstabugSample/InstabugExampleMethodCallHandler.kt @@ -37,6 +37,12 @@ class InstabugExampleMethodCallHandler : MethodChannel.MethodCallHandler { sendOOM() result.success(null) } + SET_FULLSCREEN -> { + val isEnabled = call.arguments as? Map<*, *> + val enabled = isEnabled?.get("isEnabled") as? Boolean ?: false + setFullscreen(enabled) + result.success(null) + } else -> { Log.e(TAG, "onMethodCall for ${call.method} is not implemented") result.notImplemented() @@ -55,6 +61,7 @@ class InstabugExampleMethodCallHandler : MethodChannel.MethodCallHandler { const val SEND_NATIVE_FATAL_HANG = "sendNativeFatalHang" const val SEND_ANR = "sendAnr" const val SEND_OOM = "sendOom" + const val SET_FULLSCREEN = "setFullscreen" } private fun sendNativeNonFatal(exceptionObject: String?) { @@ -125,4 +132,25 @@ class InstabugExampleMethodCallHandler : MethodChannel.MethodCallHandler { return randomString.toString() } + private fun setFullscreen(enabled: Boolean) { + try { + + try { + val instabugClass = Class.forName("com.instabug.library.Instabug") + val setFullscreenMethod = instabugClass.getMethod("setFullscreen", Boolean::class.java) + setFullscreenMethod.invoke(null, enabled) + } catch (e: ClassNotFoundException) { + throw e + } catch (e: NoSuchMethodException) { + throw e + } catch (e: Exception) { + throw e + } + + } catch (e: Exception) { + e.printStackTrace() + + } + } + } diff --git a/example/ios/InstabugTests/ApmApiTests.m b/example/ios/InstabugTests/ApmApiTests.m index ae634487d..e18643bf7 100644 --- a/example/ios/InstabugTests/ApmApiTests.m +++ b/example/ios/InstabugTests/ApmApiTests.m @@ -19,16 +19,6 @@ - (void)setUp { self.api = [[ApmApi alloc] init]; } -- (IBGExecutionTrace *)mockTraceWithId:(NSString *)traceId { - NSString* name = @"trace-name"; - IBGExecutionTrace *mTrace = OCMClassMock([IBGExecutionTrace class]); - - OCMStub([self.mAPM startExecutionTraceWithName:name]).andReturn(mTrace); - - [self.api startExecutionTraceId:traceId name:name completion:^(NSString * _Nullable _, FlutterError * _Nullable __) {}]; - - return mTrace; -} - (void)testSetEnabled { NSNumber *isEnabled = @1; @@ -116,63 +106,6 @@ - (void)testSetAutoUITraceEnabled { OCMVerify([self.mAPM setAutoUITraceEnabled:YES]); } -- (void)testStartExecutionTraceWhenTraceNotNil { - NSString *expectedId = @"trace-id"; - NSString *name = @"trace-name"; - XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; - - IBGExecutionTrace *mTrace = OCMClassMock([IBGExecutionTrace class]); - OCMStub([self.mAPM startExecutionTraceWithName:name]).andReturn(mTrace); - - [self.api startExecutionTraceId:expectedId name:name completion:^(NSString *actualId, FlutterError *error) { - [expectation fulfill]; - XCTAssertEqual(actualId, expectedId); - XCTAssertNil(error); - }]; - - OCMVerify([self.mAPM startExecutionTraceWithName:name]); - [self waitForExpectations:@[expectation] timeout:5.0]; -} - -- (void)testStartExecutionTraceWhenTraceIsNil { - NSString *traceId = @"trace-id"; - NSString *name = @"trace-name"; - XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; - - OCMStub([self.mAPM startExecutionTraceWithName:name]).andReturn(nil); - - [self.api startExecutionTraceId:traceId name:name completion:^(NSString *actualId, FlutterError *error) { - [expectation fulfill]; - XCTAssertNil(actualId); - XCTAssertNil(error); - }]; - - OCMVerify([self.mAPM startExecutionTraceWithName:name]); - [self waitForExpectations:@[expectation] timeout:5.0]; -} - - -- (void)testSetExecutionTraceAttribute { - NSString *traceId = @"trace-id"; - NSString *key = @"is_premium"; - NSString *value = @"true"; - FlutterError *error; - id mTrace = [self mockTraceWithId:traceId]; - - [self.api setExecutionTraceAttributeId:traceId key:key value:value error:&error]; - - OCMVerify([mTrace setAttributeWithKey:key value:value]); -} - -- (void)testEndExecutionTrace { - NSString *traceId = @"trace-id"; - FlutterError *error; - IBGExecutionTrace *mTrace = [self mockTraceWithId:traceId]; - - [self.api endExecutionTraceId:traceId error:&error]; - - OCMVerify([mTrace end]); -} - (void) testStartFlow { NSString* appFlowName = @"app-flow-name"; @@ -316,50 +249,41 @@ - (void)testSetScreenRenderDisabled { OCMVerify([self.mAPM setScreenRenderingEnabled:NO]); } -- (void)testDeviceRefreshRate { +- (void)testGetDeviceRefreshRateAndTolerance { XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; - // Mock UIScreen for iOS 10.3+ - id mockScreen = OCMClassMock([UIScreen class]); - OCMStub([mockScreen mainScreen]).andReturn(mockScreen); - OCMStub([mockScreen maximumFramesPerSecond]).andReturn(120.0); + // Mock values + double expectedTolerance = 5.0; + double expectedRefreshRate = 60.0; - [self.api deviceRefreshRateWithCompletion:^(NSNumber *refreshRate, FlutterError *error) { - [expectation fulfill]; - - XCTAssertEqualObjects(refreshRate, @(120.0)); - XCTAssertNil(error); - }]; - - [self waitForExpectations:@[expectation] timeout:5.0]; + // Mock the tolerance value + OCMStub([self.mAPM screenRenderingThreshold]).andReturn(expectedTolerance); - [mockScreen stopMocking]; -} - -- (void)testDeviceRefreshRateFallback { - XCTestExpectation *expectation = [self expectationWithDescription:@"Call completion handler"]; - - // Note: Testing the fallback behavior for iOS < 10.3 is challenging in unit tests - // since we can't easily mock the iOS version check. In a real scenario, this would - // return 60.0 for older iOS versions. For now, we'll test the normal case. + // Mock UIScreen class methods + id mockUIScreen = OCMClassMock([UIScreen class]); + id mockMainScreen = OCMClassMock([UIScreen class]); - // Mock UIScreen to return 60.0 (typical fallback value) - id mockScreen = OCMClassMock([UIScreen class]); - OCMStub([mockScreen mainScreen]).andReturn(mockScreen); - OCMStub([mockScreen maximumFramesPerSecond]).andReturn(60.0); + // Stub the class method and instance property + OCMStub([mockUIScreen mainScreen]).andReturn(mockMainScreen); + OCMStub([mockMainScreen maximumFramesPerSecond]).andReturn(expectedRefreshRate); - [self.api deviceRefreshRateWithCompletion:^(NSNumber *refreshRate, FlutterError *error) { + [self.api getDeviceRefreshRateAndToleranceWithCompletion:^(NSArray *result, FlutterError *error) { [expectation fulfill]; - XCTAssertEqualObjects(refreshRate, @(60.0)); + XCTAssertNotNil(result); + XCTAssertEqual(result.count, 2); + XCTAssertEqualObjects(result[0], @(expectedRefreshRate)); + XCTAssertEqualObjects(result[1], @(expectedTolerance)); XCTAssertNil(error); }]; - + [self waitForExpectations:@[expectation] timeout:5.0]; - [mockScreen stopMocking]; + [mockUIScreen stopMocking]; + [mockMainScreen stopMocking]; } + - (void)testEndScreenRenderForAutoUiTrace { FlutterError *error; diff --git a/example/ios/InstabugTests/BugReportingApiTests.m b/example/ios/InstabugTests/BugReportingApiTests.m index e01df21d2..5b6954d59 100644 --- a/example/ios/InstabugTests/BugReportingApiTests.m +++ b/example/ios/InstabugTests/BugReportingApiTests.m @@ -162,7 +162,7 @@ - (void)testSetCommentMinimumCharacterCountGivenReportTypes { [self.api setCommentMinimumCharacterCountLimit:limit reportTypes:reportTypes error:&error]; - OCMVerify([self.mBugReporting setCommentMinimumCharacterCountForReportTypes:IBGBugReportingReportTypeBug | IBGBugReportingReportTypeQuestion withLimit:limit.intValue]); + OCMVerify([self.mBugReporting setCommentMinimumCharacterCount:limit.intValue forBugReportType:IBGBugReportingReportTypeBug | IBGBugReportingReportTypeQuestion]); } - (void)testSetCommentMinimumCharacterCountGivenNoReportTypes { @@ -172,7 +172,7 @@ - (void)testSetCommentMinimumCharacterCountGivenNoReportTypes { [self.api setCommentMinimumCharacterCountLimit:limit reportTypes:reportTypes error:&error]; - OCMVerify([self.mBugReporting setCommentMinimumCharacterCountForReportTypes:IBGBugReportingReportTypeBug | IBGBugReportingReportTypeFeedback | IBGBugReportingReportTypeQuestion withLimit:limit.intValue]); + OCMVerify([self.mBugReporting setCommentMinimumCharacterCount:limit.intValue forBugReportType:IBGBugReportingReportTypeBug | IBGBugReportingReportTypeFeedback | IBGBugReportingReportTypeQuestion]); } - (void)testAddUserConsentWithKey { NSString *key = @"testKey"; diff --git a/example/ios/InstabugTests/InstabugApiTests.m b/example/ios/InstabugTests/InstabugApiTests.m index 9f2c04373..69b6343eb 100644 --- a/example/ios/InstabugTests/InstabugApiTests.m +++ b/example/ios/InstabugTests/InstabugApiTests.m @@ -200,31 +200,6 @@ - (void)testGetTags { [self waitForExpectations:@[expectation] timeout:5.0]; } -- (void)testAddExperiments { - NSArray *experiments = @[@"premium", @"star"]; - FlutterError *error; - - [self.api addExperimentsExperiments:experiments error:&error]; - - OCMVerify([self.mInstabug addExperiments:experiments]); -} - -- (void)testRemoveExperiments { - NSArray *experiments = @[@"premium", @"star"]; - FlutterError *error; - - [self.api removeExperimentsExperiments:experiments error:&error]; - - OCMVerify([self.mInstabug removeExperiments:experiments]); -} - -- (void)testClearAllExperiments { - FlutterError *error; - - [self.api clearAllExperimentsWithError:&error]; - - OCMVerify([self.mInstabug clearAllExperiments]); -} - (void)testAddFeatureFlags { NSDictionary *featureFlagsMap = @{ @"key13" : @"value1", @"key2" : @"value2"}; @@ -611,4 +586,32 @@ - (void)testisW3CFeatureFlagsEnabled { } +- (void)testSetThemeWithAllProperties { + NSDictionary *themeConfig = @{ + @"primaryColor": @"#FF6B6B", + @"backgroundColor": @"#FFFFFF", + @"titleTextColor": @"#000000", + @"primaryTextColor": @"#333333", + @"secondaryTextColor": @"#666666", + @"callToActionTextColor": @"#FF6B6B", + @"primaryFontPath": @"assets/fonts/CustomFont-Regular.ttf", + @"secondaryFontPath": @"assets/fonts/CustomFont-Bold.ttf", + @"ctaFontPath": @"assets/fonts/CustomFont-Italic.ttf" + }; + + id mockTheme = OCMClassMock([IBGTheme class]); + OCMStub([mockTheme primaryColor]).andReturn([UIColor redColor]); + OCMStub([mockTheme backgroundColor]).andReturn([UIColor whiteColor]); + OCMStub([mockTheme titleTextColor]).andReturn([UIColor blackColor]); + OCMStub([mockTheme primaryTextColor]).andReturn([UIColor darkGrayColor]); + OCMStub([mockTheme secondaryTextColor]).andReturn([UIColor grayColor]); + OCMStub([mockTheme callToActionTextColor]).andReturn([UIColor redColor]); + + FlutterError *error; + + [self.api setThemeThemeConfig:themeConfig error:&error]; + + OCMVerify([self.mInstabug setTheme:[OCMArg isNotNil]]); +} + @end diff --git a/example/ios/Podfile b/example/ios/Podfile index 9b49aa25b..bbb7ad1f7 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.23/Instabug.podspec' + pod 'Instabug', :podspec => 'https://ios-releases.instabug.com/custom/faeture-screen_rendering-release/15.1.24/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 43ffb07ea..53dc15ace 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,14 +1,14 @@ PODS: - Flutter (1.0.0) - - Instabug (15.1.23) + - Instabug (15.1.24) - instabug_flutter (14.3.0): - Flutter - - Instabug (= 15.1.23) + - Instabug (= 15.1.24) - OCMock (3.6) DEPENDENCIES: - Flutter (from `Flutter`) - - Instabug (from `https://ios-releases.instabug.com/custom/faeture-screen_rendering-release/15.1.23/Instabug.podspec`) + - Instabug (from `https://ios-releases.instabug.com/custom/faeture-screen_rendering-release/15.1.24/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.23/Instabug.podspec + :podspec: https://ios-releases.instabug.com/custom/faeture-screen_rendering-release/15.1.24/Instabug.podspec instabug_flutter: :path: ".symlinks/plugins/instabug_flutter/ios" SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - Instabug: b4659339dc6f67693cf9bd1224abc66831b8722f - instabug_flutter: eeb2e13eefca00e94de1f9156df4889f5481506a + Instabug: 81ff406348f7a9784ad2c681c94279a0ad3fcab7 + instabug_flutter: 7aeb6ad19cf4c388aef8955c3aad12ee5373adab OCMock: 5ea90566be239f179ba766fd9fbae5885040b992 -PODFILE CHECKSUM: 6d8ca5577997736d9cc2249886c9f6d10238385d +PODFILE CHECKSUM: fb14c1a442ef94a558e4e301f3ea6ba54be132dd COCOAPODS: 1.15.2 diff --git a/example/lib/main.dart b/example/lib/main.dart index ada18a241..fa332490c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -28,7 +28,6 @@ part 'src/components/non_fatal_crashes_content.dart'; part 'src/components/page.dart'; part 'src/components/screen_render.dart'; part 'src/components/screen_render_switch.dart'; -part 'src/components/traces_content.dart'; part 'src/components/ui_traces_content.dart'; part 'src/screens/apm_page.dart'; part 'src/screens/complex_page.dart'; diff --git a/example/lib/src/components/traces_content.dart b/example/lib/src/components/traces_content.dart deleted file mode 100644 index 888460d43..000000000 --- a/example/lib/src/components/traces_content.dart +++ /dev/null @@ -1,157 +0,0 @@ -part of '../../main.dart'; - -class TracesContent extends StatefulWidget { - const TracesContent({Key? key}) : super(key: key); - - @override - State createState() => _TracesContentState(); -} - -class _TracesContentState extends State { - final traceNameController = TextEditingController(); - final traceKeyAttributeController = TextEditingController(); - final traceValueAttributeController = TextEditingController(); - - bool? didTraceEnd; - - Trace? trace; - - @override - Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - return Column( - children: [ - InstabugTextField( - label: 'Trace name', - labelStyle: textTheme.labelMedium, - controller: traceNameController, - ), - SizedBox.fromSize( - size: const Size.fromHeight(10.0), - ), - Row( - children: [ - Flexible( - flex: 5, - child: InstabugButton.smallFontSize( - text: 'Start Trace', - onPressed: () => _startTrace(traceNameController.text), - margin: const EdgeInsetsDirectional.only( - start: 20.0, - end: 10.0, - ), - ), - ), - Flexible( - flex: 5, - child: InstabugButton.smallFontSize( - text: 'Start Trace With Delay', - onPressed: () => _startTrace( - traceNameController.text, - delayInMilliseconds: 5000, - ), - margin: const EdgeInsetsDirectional.only( - start: 10.0, - end: 20.0, - ), - ), - ), - ], - ), - Row( - children: [ - Flexible( - flex: 5, - child: InstabugTextField( - label: 'Trace Key Attribute', - controller: traceKeyAttributeController, - labelStyle: textTheme.labelMedium, - margin: const EdgeInsetsDirectional.only( - end: 10.0, - start: 20.0, - ), - ), - ), - Flexible( - flex: 5, - child: InstabugTextField( - label: 'Trace Value Attribute', - labelStyle: textTheme.labelMedium, - controller: traceValueAttributeController, - margin: const EdgeInsetsDirectional.only( - start: 10.0, - end: 20.0, - ), - ), - ), - ], - ), - SizedBox.fromSize( - size: const Size.fromHeight(10.0), - ), - InstabugButton( - text: 'Set Trace Attribute', - onPressed: () => _setTraceAttribute( - trace, - traceKeyAttribute: traceKeyAttributeController.text, - traceValueAttribute: traceValueAttributeController.text, - ), - ), - InstabugButton( - text: 'End Trace', - onPressed: () => _endTrace(), - ), - ], - ); - } - - void _startTrace( - String traceName, { - int delayInMilliseconds = 0, - }) { - if (traceName.trim().isNotEmpty) { - log('_startTrace — traceName: $traceName, delay in Milliseconds: $delayInMilliseconds'); - log('traceName: $traceName'); - Future.delayed( - Duration(milliseconds: delayInMilliseconds), - () => APM - .startExecutionTrace(traceName) - .then((value) => trace = value)); - } else { - log('startTrace - Please enter a trace name'); - } - } - - void _endTrace() { - if (didTraceEnd == true) { - log('_endTrace — Please, start a new trace before setting attributes.'); - } - if (trace == null) { - log('_endTrace — Please, start a trace before ending it.'); - } - log('_endTrace — ending Trace.'); - trace?.end(); - didTraceEnd = true; - } - - void _setTraceAttribute( - Trace? trace, { - required String traceKeyAttribute, - required String traceValueAttribute, - }) { - if (trace == null) { - log('_setTraceAttribute — Please, start a trace before setting attributes.'); - } - if (didTraceEnd == true) { - log('_setTraceAttribute — Please, start a new trace before setting attributes.'); - } - if (traceKeyAttribute.trim().isEmpty) { - log('_setTraceAttribute — Please, fill the trace key attribute input before settings attributes.'); - } - if (traceValueAttribute.trim().isEmpty) { - log('_setTraceAttribute — Please, fill the trace value attribute input before settings attributes.'); - } - log('_setTraceAttribute — setting attributes -> key: $traceKeyAttribute, value: $traceValueAttribute.'); - trace?.setAttribute(traceKeyAttribute, traceValueAttribute); - } -} diff --git a/example/lib/src/native/instabug_flutter_example_method_channel.dart b/example/lib/src/native/instabug_flutter_example_method_channel.dart index 9507cc403..118097dc3 100644 --- a/example/lib/src/native/instabug_flutter_example_method_channel.dart +++ b/example/lib/src/native/instabug_flutter_example_method_channel.dart @@ -54,6 +54,22 @@ class InstabugFlutterExampleMethodChannel { log("Failed to send out of memory: '${e.message}'.", name: _tag); } } + + static Future setFullscreen(bool isEnabled) async { + if (!Platform.isAndroid) { + return; + } + + try { + await _channel.invokeMethod(Constants.setFullscreenMethodName, { + 'isEnabled': isEnabled, + }); + } on PlatformException catch (e) { + log("Failed to set fullscreen: '${e.message}'.", name: _tag); + } catch (e) { + log("Unexpected error setting fullscreen: '$e'.", name: _tag); + } + } } class Constants { @@ -65,4 +81,5 @@ class Constants { static const sendNativeFatalHangMethodName = "sendNativeFatalHang"; static const sendAnrMethodName = "sendAnr"; static const sendOomMethodName = "sendOom"; + static const setFullscreenMethodName = "setFullscreen"; } diff --git a/example/lib/src/screens/apm_page.dart b/example/lib/src/screens/apm_page.dart index b5de7a228..1c0c3ce05 100644 --- a/example/lib/src/screens/apm_page.dart +++ b/example/lib/src/screens/apm_page.dart @@ -36,8 +36,6 @@ class _ApmPageState extends State { ), const SectionTitle('Network'), const NetworkContent(), - const SectionTitle('Traces'), - const TracesContent(), const SectionTitle('Flows'), const FlowsContent(), const SectionTitle('Custom UI Traces'), diff --git a/example/lib/src/screens/my_home_page.dart b/example/lib/src/screens/my_home_page.dart index 404d79cdd..5f7d50a88 100644 --- a/example/lib/src/screens/my_home_page.dart +++ b/example/lib/src/screens/my_home_page.dart @@ -114,10 +114,10 @@ class _MyHomePageState extends State { BugReporting.setInvocationEvents([invocationEvent]); } - void changePrimaryColor() { - String text = 'FF' + primaryColorController.text.replaceAll('#', ''); - Color color = Color(int.parse(text, radix: 16)); - Instabug.setPrimaryColor(color); + void changePrimaryColor() async { + String text = primaryColorController.text.replaceAll('#', ''); + await Instabug.setTheme(ThemeConfig(primaryColor: '#$text')); + await Future.delayed(const Duration(milliseconds: 500)); } void setColorTheme(ColorTheme colorTheme) { diff --git a/example/pubspec.lock b/example/pubspec.lock index dbdfc1d49..ed56bbe53 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,62 +1,76 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.6" async: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.13.0" + version: "2.8.2" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.0" characters: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.3.1" clock: dependency: transitive description: name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.1.0" collection: dependency: transitive description: name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.19.1" + version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" fake_async: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.3.3" + version: "1.2.0" file: dependency: transitive description: name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "7.0.1" + version: "6.1.2" flutter: dependency: "direct main" description: flutter @@ -71,8 +85,7 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.0.4" flutter_test: @@ -89,18 +102,16 @@ packages: dependency: "direct main" description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.13.6" + version: "0.13.5" http_parser: dependency: transitive description: name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "4.1.2" + version: "4.0.2" instabug_flutter: dependency: "direct main" description: @@ -112,183 +123,139 @@ packages: dependency: "direct main" description: name: instabug_http_client - sha256: "97a6ab88491eff87e42437564b528d6986a65eb3f3262f73373009f949cb4560" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.5.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" - url: "https://pub.dev" - source: hosted - version: "10.0.9" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 - url: "https://pub.dev" - source: hosted - version: "3.0.9" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" - source: hosted - version: "3.0.1" + version: "2.6.0" lints: dependency: transitive description: name: lints - sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" matcher: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.12.17" + version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.11.1" + version: "0.1.3" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.16.0" + version: "1.7.0" path: dependency: transitive description: name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.9.1" + version: "1.8.0" platform: dependency: transitive description: name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "3.1.6" + version: "3.1.0" process: dependency: transitive description: name: process - sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "5.0.3" + version: "4.2.4" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.0" + version: "0.0.99" source_span: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.10.1" + version: "1.8.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.12.1" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.4.1" + version: "1.1.0" sync_http: dependency: transitive description: name: sync_http - sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.3.1" + version: "0.3.0" term_glyph: dependency: transitive description: name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.2.2" + version: "1.2.0" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.7.4" + version: "0.4.8" typed_data: dependency: transitive description: name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.3.0" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.1" vm_service: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "15.0.0" + version: "7.5.0" webdriver: dependency: transitive description: name: webdriver - sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "3.0.0" sdks: - dart: ">=3.7.0-0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + dart: ">=2.14.0 <3.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index fe72aaa2d..dfd49f2aa 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -51,6 +51,7 @@ flutter: # To add assets to your application, add an assets section, like this: # assets: + # - assets/fonts/ # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg @@ -66,6 +67,9 @@ flutter: # list giving the asset and other descriptors for the font. For # example: # fonts: + # - family: ManufacturingConsent + # fonts: + # - asset: assets/fonts/ManufacturingConsent-Regular.ttf # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf diff --git a/ios/Classes/Modules/ApmApi.m b/ios/Classes/Modules/ApmApi.m index 4787a9b22..b4d6bd161 100644 --- a/ios/Classes/Modules/ApmApi.m +++ b/ios/Classes/Modules/ApmApi.m @@ -70,44 +70,6 @@ - (void)setAutoUITraceEnabledIsEnabled:(NSNumber *)isEnabled error:(FlutterError IBGAPM.autoUITraceEnabled = [isEnabled boolValue]; } -// This method is responsible for starting an execution trace -// with a given `id` and `name`. -// -// Deprecated - see [startFlowName, setFlowAttributeName & endFlowName]. -- (void)startExecutionTraceId:(NSString *)id name:(NSString *)name completion:(void(^)(NSString *_Nullable, FlutterError *_Nullable))completion { - IBGExecutionTrace *trace = [IBGAPM startExecutionTraceWithName:name]; - - if (trace != nil) { - [traces setObject:trace forKey:id]; - return completion(id, nil); - } else { - return completion(nil, nil); - } -} - -// This method is responsible for setting an attribute for a specific -// execution trace identified by the provided `id`. -// -// Deprecated - see [startFlowName, setFlowAttributeName & endFlowName]. -- (void)setExecutionTraceAttributeId:(NSString *)id key:(NSString *)key value:(NSString *)value error:(FlutterError *_Nullable *_Nonnull)error { - IBGExecutionTrace *trace = [traces objectForKey:id]; - - if (trace != nil) { - [trace setAttributeWithKey:key value:value]; - } -} - -// This method `endExecutionTraceId` is responsible for ending an execution trace identified by the -// provided `id`. -// -// Deprecated - see [startFlowName, setFlowAttributeName & endFlowName]. -- (void)endExecutionTraceId:(NSString *)id error:(FlutterError *_Nullable *_Nonnull)error { - IBGExecutionTrace *trace = [traces objectForKey:id]; - - if (trace != nil) { - [trace end]; - } -} // This method is responsible for starting a flow with the given `name`. This functionality is used to // track and monitor the performance of specific flows within the application. @@ -205,23 +167,14 @@ - (void)isScreenRenderEnabledWithCompletion:(void (^)(NSNumber * _Nullable, Flut - (void)setScreenRenderEnabledIsEnabled:(nonnull NSNumber *)isEnabled error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { [IBGAPM setScreenRenderingEnabled:[isEnabled boolValue]]; - -} -- (void)deviceRefreshRateWithCompletion:(void (^)(NSNumber * _Nullable, FlutterError * _Nullable))completion{ - if (@available(iOS 10.3, *)) { - double refreshRate = [UIScreen mainScreen].maximumFramesPerSecond; - completion(@(refreshRate) ,nil); - } else { - // Fallback for very old iOS versions. - completion(@(60.0) , nil); - } } + - (void)endScreenRenderForAutoUiTraceData:(nonnull NSDictionary *)data error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { NSArray *> *rawFrames = data[@"frameData"]; NSMutableArray *frameInfos = [[NSMutableArray alloc] init]; - + if (rawFrames && [rawFrames isKindOfClass:[NSArray class]]) { for (NSArray *frameValues in rawFrames) { if ([frameValues count] == 2) { @@ -239,7 +192,7 @@ - (void)endScreenRenderForAutoUiTraceData:(nonnull NSDictionary * - (void)endScreenRenderForCustomUiTraceData:(nonnull NSDictionary *)data error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { NSArray *> *rawFrames = data[@"frameData"]; NSMutableArray *frameInfos = [[NSMutableArray alloc] init]; - + if (rawFrames && [rawFrames isKindOfClass:[NSArray class]]) { for (NSArray *frameValues in rawFrames) { if ([frameValues count] == 2) { @@ -250,10 +203,22 @@ - (void)endScreenRenderForCustomUiTraceData:(nonnull NSDictionary } } } - + [IBGAPM endCustomUITraceCPWithFrames:frameInfos]; } +- (void)getDeviceRefreshRateAndToleranceWithCompletion:(nonnull void (^)(NSArray * _Nullable, FlutterError * _Nullable))completion { + double tolerance = IBGAPM.screenRenderingThreshold; + if (@available(iOS 10.3, *)) { + double refreshRate = [UIScreen mainScreen].maximumFramesPerSecond; + completion(@[@(refreshRate), @(tolerance)] ,nil); + } else { + // Fallback for very old iOS versions. + completion(@[@(60.0), @(tolerance)] , nil); + } +} + + @end diff --git a/ios/Classes/Modules/BugReportingApi.m b/ios/Classes/Modules/BugReportingApi.m index bb97810b8..7a92a9563 100644 --- a/ios/Classes/Modules/BugReportingApi.m +++ b/ios/Classes/Modules/BugReportingApi.m @@ -151,8 +151,7 @@ - (void)setDisclaimerTextText:(NSString *)text error:(FlutterError *_Nullable *_ } - (void)setCommentMinimumCharacterCountLimit:(NSNumber *)limit reportTypes:(nullable NSArray *)reportTypes error:(FlutterError *_Nullable *_Nonnull)error { - IBGBugReportingReportType resolvedTypes = 0; - + IBGBugReportingType resolvedTypes = 0; if (![reportTypes count]) { resolvedTypes = (ArgsRegistry.reportTypes[@"ReportType.bug"]).integerValue | (ArgsRegistry.reportTypes[@"ReportType.feedback"]).integerValue | (ArgsRegistry.reportTypes[@"ReportType.question"]).integerValue; } @@ -162,7 +161,7 @@ - (void)setCommentMinimumCharacterCountLimit:(NSNumber *)limit reportTypes:(null } } - [IBGBugReporting setCommentMinimumCharacterCountForReportTypes:resolvedTypes withLimit:limit.intValue]; + [IBGBugReporting setCommentMinimumCharacterCount:[limit integerValue] forBugReportType:resolvedTypes]; } - (void)addUserConsentsKey:(NSString *)key diff --git a/ios/Classes/Modules/InstabugApi.m b/ios/Classes/Modules/InstabugApi.m index 3bdc465f0..287071683 100644 --- a/ios/Classes/Modules/InstabugApi.m +++ b/ios/Classes/Modules/InstabugApi.m @@ -14,7 +14,9 @@ extern void InitInstabugApi(id messenger) { InstabugHostApiSetup(messenger, api); } -@implementation InstabugApi +@implementation InstabugApi { + NSMutableSet *_registeredFonts; +} - (void)setEnabledIsEnabled:(NSNumber *)isEnabled error:(FlutterError *_Nullable *_Nonnull)error { Instabug.enabled = [isEnabled boolValue]; @@ -127,17 +129,7 @@ - (void)getTagsWithCompletion:(nonnull void (^)(NSArray * _Nullable, completion([Instabug getTags], nil); } -- (void)addExperimentsExperiments:(NSArray *)experiments error:(FlutterError *_Nullable *_Nonnull)error { - [Instabug addExperiments:experiments]; -} - -- (void)removeExperimentsExperiments:(NSArray *)experiments error:(FlutterError *_Nullable *_Nonnull)error { - [Instabug removeExperiments:experiments]; -} -- (void)clearAllExperimentsWithError:(FlutterError *_Nullable *_Nonnull)error { - [Instabug clearAllExperiments]; -} - (void)setUserAttributeValue:(NSString *)value key:(NSString *)key error:(FlutterError *_Nullable *_Nonnull)error { [Instabug setUserAttribute:value withKey:key]; @@ -396,4 +388,172 @@ - (void)setNetworkLogBodyEnabledIsEnabled:(NSNumber *)isEnabled IBGNetworkLogger.logBodyEnabled = [isEnabled boolValue]; } + + +- (void)setThemeThemeConfig:(NSDictionary *)themeConfig error:(FlutterError *_Nullable *_Nonnull)error { + IBGTheme *theme = [[IBGTheme alloc] init]; + + NSDictionary *colorMapping = @{ + @"primaryColor": ^(UIColor *color) { theme.primaryColor = color; }, + @"backgroundColor": ^(UIColor *color) { theme.backgroundColor = color; }, + @"titleTextColor": ^(UIColor *color) { theme.titleTextColor = color; }, + @"subtitleTextColor": ^(UIColor *color) { theme.subtitleTextColor = color; }, + @"primaryTextColor": ^(UIColor *color) { theme.primaryTextColor = color; }, + @"secondaryTextColor": ^(UIColor *color) { theme.secondaryTextColor = color; }, + @"callToActionTextColor": ^(UIColor *color) { theme.callToActionTextColor = color; }, + @"headerBackgroundColor": ^(UIColor *color) { theme.headerBackgroundColor = color; }, + @"footerBackgroundColor": ^(UIColor *color) { theme.footerBackgroundColor = color; }, + @"rowBackgroundColor": ^(UIColor *color) { theme.rowBackgroundColor = color; }, + @"selectedRowBackgroundColor": ^(UIColor *color) { theme.selectedRowBackgroundColor = color; }, + @"rowSeparatorColor": ^(UIColor *color) { theme.rowSeparatorColor = color; } + }; + + for (NSString *key in colorMapping) { + if (themeConfig[key]) { + NSString *colorString = themeConfig[key]; + UIColor *color = [self colorFromHexString:colorString]; + if (color) { + void (^setter)(UIColor *) = colorMapping[key]; + setter(color); + } + } + } + + [self setFontIfPresent:themeConfig[@"primaryFontPath"] ?: themeConfig[@"primaryFontAsset"] forTheme:theme type:@"primary"]; + [self setFontIfPresent:themeConfig[@"secondaryFontPath"] ?: themeConfig[@"secondaryFontAsset"] forTheme:theme type:@"secondary"]; + [self setFontIfPresent:themeConfig[@"ctaFontPath"] ?: themeConfig[@"ctaFontAsset"] forTheme:theme type:@"cta"]; + + Instabug.theme = theme; +} + +- (void)setFontIfPresent:(NSString *)fontPath forTheme:(IBGTheme *)theme type:(NSString *)type { + if (!fontPath || fontPath.length == 0 || !theme || !type) return; + + if (!_registeredFonts) { + _registeredFonts = [NSMutableSet set]; + } + + // Check if font is already registered + if ([_registeredFonts containsObject:fontPath]) { + UIFont *font = [UIFont fontWithName:fontPath size:UIFont.systemFontSize]; + if (font) { + [self setFont:font forTheme:theme type:type]; + } + return; + } + + // Try to load font from system fonts first + UIFont *font = [UIFont fontWithName:fontPath size:UIFont.systemFontSize]; + if (font) { + [_registeredFonts addObject:fontPath]; + [self setFont:font forTheme:theme type:type]; + return; + } + + // Try to load font from bundle + font = [self loadFontFromPath:fontPath]; + if (font) { + [_registeredFonts addObject:fontPath]; + [self setFont:font forTheme:theme type:type]; + } +} + +- (UIFont *)loadFontFromPath:(NSString *)fontPath { + NSString *fontFileName = [fontPath stringByDeletingPathExtension]; + NSArray *fontExtensions = @[@"ttf", @"otf", @"woff", @"woff2"]; + + // Find font file in bundle + NSString *fontFilePath = nil; + for (NSString *extension in fontExtensions) { + fontFilePath = [[NSBundle mainBundle] pathForResource:fontFileName ofType:extension]; + if (fontFilePath) break; + } + + if (!fontFilePath) { + return nil; + } + + // Load font data + NSData *fontData = [NSData dataWithContentsOfFile:fontFilePath]; + if (!fontData) { + return nil; + } + + // Create data provider + CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)fontData); + if (!provider) { + return nil; + } + + // Create CG font + CGFontRef cgFont = CGFontCreateWithDataProvider(provider); + CGDataProviderRelease(provider); + + if (!cgFont) { + return nil; + } + + // Register font + CFErrorRef error = NULL; + BOOL registered = CTFontManagerRegisterGraphicsFont(cgFont, &error); + + if (!registered) { + if (error) { + CFStringRef description = CFErrorCopyDescription(error); + CFRelease(description); + CFRelease(error); + } + CGFontRelease(cgFont); + return nil; + } + + // Get PostScript name and create UIFont + NSString *postScriptName = (__bridge_transfer NSString *)CGFontCopyPostScriptName(cgFont); + CGFontRelease(cgFont); + + if (!postScriptName) { + return nil; + } + + return [UIFont fontWithName:postScriptName size:UIFont.systemFontSize]; +} + +- (void)setFont:(UIFont *)font forTheme:(IBGTheme *)theme type:(NSString *)type { + if (!font || !theme || !type) return; + + if ([type isEqualToString:@"primary"]) { + theme.primaryTextFont = font; + } else if ([type isEqualToString:@"secondary"]) { + theme.secondaryTextFont = font; + } else if ([type isEqualToString:@"cta"]) { + theme.callToActionTextFont = font; + } +} + +- (UIColor *)colorFromHexString:(NSString *)hexString { + NSString *cleanString = [hexString stringByReplacingOccurrencesOfString:@"#" withString:@""]; + + if (cleanString.length == 6) { + unsigned int rgbValue = 0; + NSScanner *scanner = [NSScanner scannerWithString:cleanString]; + [scanner scanHexInt:&rgbValue]; + + return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16) / 255.0 + green:((rgbValue & 0xFF00) >> 8) / 255.0 + blue:(rgbValue & 0xFF) / 255.0 + alpha:1.0]; + } else if (cleanString.length == 8) { + unsigned int rgbaValue = 0; + NSScanner *scanner = [NSScanner scannerWithString:cleanString]; + [scanner scanHexInt:&rgbaValue]; + + return [UIColor colorWithRed:((rgbaValue & 0xFF000000) >> 24) / 255.0 + green:((rgbaValue & 0xFF0000) >> 16) / 255.0 + blue:((rgbaValue & 0xFF00) >> 8) / 255.0 + alpha:(rgbaValue & 0xFF) / 255.0]; + } + + return [UIColor blackColor]; +} + @end diff --git a/ios/Classes/Util/IBGAPM+PrivateAPIs.h b/ios/Classes/Util/IBGAPM+PrivateAPIs.h index c562e629e..bc8a1e90a 100644 --- a/ios/Classes/Util/IBGAPM+PrivateAPIs.h +++ b/ios/Classes/Util/IBGAPM+PrivateAPIs.h @@ -31,4 +31,6 @@ + (void)endCustomUITraceCPWithFrames:(nullable NSArray *)frames; ++ (double)screenRenderingThreshold; + @end diff --git a/ios/instabug_flutter.podspec b/ios/instabug_flutter.podspec index b4625d3ad..20348234e 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.23' + s.dependency 'Instabug', '15.1.24' end diff --git a/lib/instabug_flutter.dart b/lib/instabug_flutter.dart index d2df80429..345aab2ef 100644 --- a/lib/instabug_flutter.dart +++ b/lib/instabug_flutter.dart @@ -3,7 +3,7 @@ export 'src/models/crash_data.dart'; export 'src/models/exception_data.dart'; export 'src/models/feature_flag.dart'; export 'src/models/network_data.dart'; -export 'src/models/trace.dart'; +export 'src/models/theme_config.dart'; export 'src/models/w3c_header.dart'; // Modules export 'src/modules/apm.dart'; diff --git a/lib/src/models/theme_config.dart b/lib/src/models/theme_config.dart new file mode 100644 index 000000000..ea3a6754e --- /dev/null +++ b/lib/src/models/theme_config.dart @@ -0,0 +1,121 @@ +class ThemeConfig { + /// Primary color for UI elements indicating interactivity or call to action. + final String? primaryColor; + + /// Background color for the main UI. + final String? backgroundColor; + + /// Color for title text elements. + final String? titleTextColor; + + /// Color for subtitle text elements. + final String? subtitleTextColor; + + /// Color for primary text elements. + final String? primaryTextColor; + + /// Color for secondary text elements. + final String? secondaryTextColor; + + /// Color for call-to-action text elements. + final String? callToActionTextColor; + + /// Background color for header elements. + final String? headerBackgroundColor; + + /// Background color for footer elements. + final String? footerBackgroundColor; + + /// Background color for row elements. + final String? rowBackgroundColor; + + /// Background color for selected row elements. + final String? selectedRowBackgroundColor; + + /// Color for row separator lines. + final String? rowSeparatorColor; + + /// Text style for primary text (Android only). + final String? primaryTextStyle; + + /// Text style for secondary text (Android only). + final String? secondaryTextStyle; + + /// Text style for title text (Android only). + final String? titleTextStyle; + + /// Text style for call-to-action text (Android only). + final String? ctaTextStyle; + + /// Path to primary font file. + final String? primaryFontPath; + + /// Asset path to primary font file. + final String? primaryFontAsset; + + /// Path to secondary font file. + final String? secondaryFontPath; + + /// Asset path to secondary font file. + final String? secondaryFontAsset; + + /// Path to call-to-action font file. + final String? ctaFontPath; + + /// Asset path to call-to-action font file. + final String? ctaFontAsset; + + const ThemeConfig({ + this.primaryColor, + this.backgroundColor, + this.titleTextColor, + this.subtitleTextColor, + this.primaryTextColor, + this.secondaryTextColor, + this.callToActionTextColor, + this.headerBackgroundColor, + this.footerBackgroundColor, + this.rowBackgroundColor, + this.selectedRowBackgroundColor, + this.rowSeparatorColor, + this.primaryTextStyle, + this.secondaryTextStyle, + this.titleTextStyle, + this.ctaTextStyle, + this.primaryFontPath, + this.primaryFontAsset, + this.secondaryFontPath, + this.secondaryFontAsset, + this.ctaFontPath, + this.ctaFontAsset, + }); + + Map toMap() { + return Map.fromEntries( + [ + MapEntry('primaryColor', primaryColor), + MapEntry('backgroundColor', backgroundColor), + MapEntry('titleTextColor', titleTextColor), + MapEntry('subtitleTextColor', subtitleTextColor), + MapEntry('primaryTextColor', primaryTextColor), + MapEntry('secondaryTextColor', secondaryTextColor), + MapEntry('callToActionTextColor', callToActionTextColor), + MapEntry('headerBackgroundColor', headerBackgroundColor), + MapEntry('footerBackgroundColor', footerBackgroundColor), + MapEntry('rowBackgroundColor', rowBackgroundColor), + MapEntry('selectedRowBackgroundColor', selectedRowBackgroundColor), + MapEntry('rowSeparatorColor', rowSeparatorColor), + MapEntry('primaryTextStyle', primaryTextStyle), + MapEntry('secondaryTextStyle', secondaryTextStyle), + MapEntry('titleTextStyle', titleTextStyle), + MapEntry('ctaTextStyle', ctaTextStyle), + MapEntry('primaryFontPath', primaryFontPath), + MapEntry('primaryFontAsset', primaryFontAsset), + MapEntry('secondaryFontPath', secondaryFontPath), + MapEntry('secondaryFontAsset', secondaryFontAsset), + MapEntry('ctaFontPath', ctaFontPath), + MapEntry('ctaFontAsset', ctaFontAsset), + ].where((entry) => entry.value != null), + ); + } +} diff --git a/lib/src/models/trace.dart b/lib/src/models/trace.dart deleted file mode 100644 index bf267640b..000000000 --- a/lib/src/models/trace.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:instabug_flutter/src/modules/apm.dart'; - -class Trace { - Trace({ - required this.id, - required this.name, - }); - - final String id; - final String name; - final Map attributes = {}; - - /// Sets attribute of execution trace. - /// [String] id of the trace. - /// [String] key of attribute. - /// [String] value of attribute. - /// - /// Please migrate to the App Flows APIs: [APM.startFlow], [APM.setFlowAttribute], and [APM.endFlow]. - @Deprecated( - 'Please migrate to the App Flows APIs: APM.startAppFlow, APM.endFlow, and APM.setFlowAttribute. This feature was deprecated in v13.0.0', - ) - void setAttribute(String key, String value) { - APM.setExecutionTraceAttribute(id, key, value); - attributes[key] = value; - } - - /// Ends Execution Trace - /// - /// Please migrate to the App Flows APIs: [APM.startFlow], [APM.setFlowAttribute], and [APM.endFlow]. - @Deprecated( - 'Please migrate to the App Flows APIs: APM.startAppFlow, APM.endFlow, and APM.setFlowAttribute. This feature was deprecated in v13.0.0', - ) - void end() { - APM.endExecutionTrace(id); - } - - Map toJson() { - return { - 'id': id, - 'name': name, - 'attributes': attributes, - }; - } -} diff --git a/lib/src/modules/apm.dart b/lib/src/modules/apm.dart index 05b2298c8..09b69ed68 100644 --- a/lib/src/modules/apm.dart +++ b/lib/src/modules/apm.dart @@ -6,9 +6,7 @@ import 'package:flutter/widgets.dart' show WidgetBuilder, WidgetsBinding; import 'package:instabug_flutter/src/generated/apm.api.g.dart'; import 'package:instabug_flutter/src/models/instabug_screen_render_data.dart'; import 'package:instabug_flutter/src/models/network_data.dart'; -import 'package:instabug_flutter/src/models/trace.dart'; import 'package:instabug_flutter/src/utils/ibg_build_info.dart'; -import 'package:instabug_flutter/src/utils/ibg_date_time.dart'; import 'package:instabug_flutter/src/utils/instabug_logger.dart'; import 'package:instabug_flutter/src/utils/screen_loading/screen_loading_manager.dart'; import 'package:instabug_flutter/src/utils/screen_rendering/instabug_screen_render_manager.dart'; @@ -75,58 +73,6 @@ class APM { return _host.setColdAppLaunchEnabled(isEnabled); } - /// Starts an execution trace. - /// [String] name of the trace. - /// - /// Please migrate to the App Flows APIs: [startFlow], [setFlowAttribute], and [endFlow]. - @Deprecated( - 'Please migrate to the App Flows APIs: APM.startAppFlow, APM.endFlow, and APM.setFlowAttribute. This feature was deprecated in v13.0.0', - ) - static Future startExecutionTrace(String name) async { - final id = IBGDateTime.instance.now(); - final traceId = await _host.startExecutionTrace(id.toString(), name); - - if (traceId == null) { - return Future.error( - "Execution trace $name wasn't created. Please make sure to enable APM first by following " - 'the instructions at this link: https://docs.instabug.com/reference#enable-or-disable-apm', - ); - } - - return Trace( - id: traceId, - name: name, - ); - } - - /// Sets attribute of an execution trace. - /// [String] id of the trace. - /// [String] key of attribute. - /// [String] value of attribute. - /// - /// Please migrate to the App Flows APIs: [startFlow], [setFlowAttribute], and [endFlow]. - @Deprecated( - 'Please migrate to the App Flows APIs: APM.startAppFlow, APM.endFlow, and APM.setFlowAttribute. This feature was deprecated in v13.0.0', - ) - static Future setExecutionTraceAttribute( - String id, - String key, - String value, - ) async { - return _host.setExecutionTraceAttribute(id, key, value); - } - - /// Ends an execution trace. - /// [String] id of the trace. - /// - /// Please migrate to the App Flows APIs: [startFlow], [setFlowAttribute], and [endFlow]. - @Deprecated( - 'Please migrate to the App Flows APIs: APM.startAppFlow, APM.endFlow, and APM.setFlowAttribute. This feature was deprecated in v13.0.0', - ) - static Future endExecutionTrace(String id) async { - return _host.endExecutionTrace(id); - } - /// Starts an AppFlow with the given [name]. /// /// The [name] must not be an empty string. It should be unique and not exceed 150 characters, @@ -196,9 +142,9 @@ class APM { (_) async { // Start screen render collector for custom ui trace if enabled. if (await FlagsConfig.screenRendering.isEnabled()) { - InstabugScreenRenderManager.I.endScreenRenderCollector(); + InstabugScreenRenderManager.I + .endScreenRenderCollector(UiTraceType.custom); - // final uiTraceId = IBGDateTime.I.now().millisecondsSinceEpoch; InstabugScreenRenderManager.I .startScreenRenderCollectorForTraceId(0, UiTraceType.custom); } @@ -213,7 +159,8 @@ class APM { static Future endUITrace() async { // End screen render collector for custom ui trace if enabled. if (InstabugScreenRenderManager.I.screenRenderEnabled) { - return InstabugScreenRenderManager.I.endScreenRenderCollector(); + return InstabugScreenRenderManager.I + .endScreenRenderCollector(UiTraceType.custom); } return _host.endUITrace(); @@ -380,9 +327,13 @@ class APM { /// /// Returns: /// A Future that represent the refresh rate. + /// Retrieve the device refresh rate and tolerance value from native side. + /// + /// Returns: + /// A Future> where the first element is the refresh rate and the second is the tolerance value. @internal - static Future getDeviceRefreshRate() { - return _host.deviceRefreshRate(); + static Future> getDeviceRefreshRateAndTolerance() { + return _host.getDeviceRefreshRateAndTolerance(); } /// Sets the screen Render state based on the provided boolean value. diff --git a/lib/src/modules/instabug.dart b/lib/src/modules/instabug.dart index 68658985c..35aea73a0 100644 --- a/lib/src/modules/instabug.dart +++ b/lib/src/modules/instabug.dart @@ -263,31 +263,6 @@ class Instabug { return tags?.cast(); } - /// Adds experiments to the next report. - @Deprecated( - 'Please migrate to the new feature flags APIs: Instabug.addFeatureFlags.', - ) - static Future addExperiments(List experiments) async { - return _host.addExperiments(experiments); - } - - /// Removes certain experiments from the next report. - @Deprecated( - 'Please migrate to the new feature flags APIs: Instabug.removeFeatureFlags.', - ) - static Future removeExperiments(List experiments) async { - return _host.removeExperiments(experiments); - } - - /// Clears all experiments from the next report. - - @Deprecated( - 'Please migrate to the new feature flags APIs: Instabug.clearAllFeatureFlags.', - ) - static Future clearAllExperiments() async { - return _host.clearAllExperiments(); - } - /// Adds feature flags to the next report. static Future addFeatureFlags(List featureFlags) async { final map = {}; @@ -363,8 +338,13 @@ class Instabug { /// Sets the primary color of the SDK's UI. /// Sets the color of UI elements indicating interactivity or call to action. /// [color] primaryColor A color to set the UI elements of the SDK to. + /// + /// Note: This API is deprecated. Please use `Instabug.setTheme` instead. + @Deprecated( + 'This API is deprecated. Please use Instabug.setTheme instead.', + ) static Future setPrimaryColor(Color color) async { - return _host.setPrimaryColor(color.value); + await setTheme(ThemeConfig(primaryColor: color.toString())); } /// Adds specific user data that you need to be added to the reports @@ -487,4 +467,32 @@ class Instabug { static Future willRedirectToStore() async { return _host.willRedirectToStore(); } + + /// Sets a custom theme for Instabug UI elements. + /// + /// @param theme - Configuration object containing theme properties + /// + /// Example: + /// ```dart + /// + /// Instabug.setTheme(ThemeConfig( + /// primaryColor: '#FF6B6B', + /// secondaryTextColor: '#666666', + /// primaryTextColor: '#333333', + /// titleTextColor: '#000000', + /// backgroundColor: '#FFFFFF', + /// primaryTextStyle: 'bold', + /// secondaryTextStyle: 'normal', + /// titleTextStyle: 'bold', + /// ctaTextStyle: 'bold', + /// primaryFontPath: '/data/user/0/com.yourapp/files/fonts/YourFont.ttf', + /// secondaryFontPath: '/data/user/0/com.yourapp/files/fonts/YourFont.ttf', + /// ctaFontPath: '/data/user/0/com.yourapp/files/fonts/YourFont.ttf', + /// primaryFontAsset: 'fonts/YourFont.ttf', + /// secondaryFontAsset: 'fonts/YourFont.ttf' + /// )); + /// ``` + static Future setTheme(ThemeConfig themeConfig) async { + return _host.setTheme(themeConfig.toMap()); + } } diff --git a/lib/src/utils/screen_rendering/instabug_screen_render_manager.dart b/lib/src/utils/screen_rendering/instabug_screen_render_manager.dart index 4876d0b15..3a9ff003d 100644 --- a/lib/src/utils/screen_rendering/instabug_screen_render_manager.dart +++ b/lib/src/utils/screen_rendering/instabug_screen_render_manager.dart @@ -34,9 +34,6 @@ class InstabugScreenRenderManager { final List _delayedFrames = []; - /// 1 / DeviceRefreshRate * 1000 - double _deviceRefreshRate = 60; - /// Default refresh rate for 60 FPS displays in milliseconds (16.67ms) double _slowFrameThresholdMs = 16.67; @@ -128,9 +125,7 @@ class InstabugScreenRenderManager { ]) { try { // Return if frameTimingListener not attached - if (!screenRenderEnabled || !_isTimingsListenerAttached) { - return; - } + if (frameCollectorIsNotActive) return; if (type == UiTraceType.custom) { _screenRenderForCustomUiTrace.traceId = traceId; @@ -150,9 +145,8 @@ class InstabugScreenRenderManager { ]) { try { // Return if frameTimingListener not attached - if (!screenRenderEnabled || !_isTimingsListenerAttached) { - return; - } + // log("frameCollectorIsNotActive $frameCollectorIsNotActive"); + if (frameCollectorIsNotActive) return; //Save the memory cached data to be sent to native side if (_delayedFrames.isNotEmpty) { @@ -181,24 +175,33 @@ class InstabugScreenRenderManager { @internal void stopScreenRenderCollector() { try { + // Return if frameTimingListener not attached + if (frameCollectorIsNotActive) return; + if (_delayedFrames.isNotEmpty) { _saveCollectedData(); + _resetCachedFrameData(); } // Sync Screen Render data for custom ui trace if exists if (_screenRenderForCustomUiTrace.isActive) { _reportScreenRenderForCustomUiTrace(_screenRenderForCustomUiTrace); + _screenRenderForCustomUiTrace.clear(); } // Sync Screen Render data for auto ui trace if exists if (_screenRenderForAutoUiTrace.isActive) { _reportScreenRenderForAutoUiTrace(_screenRenderForAutoUiTrace); + _screenRenderForAutoUiTrace.clear(); } } catch (error, stackTrace) { _logExceptionErrorAndStackTrace(error, stackTrace); } } + bool get frameCollectorIsNotActive => + !screenRenderEnabled || !_isTimingsListenerAttached; + /// Dispose InstabugScreenRenderManager by removing timings callback and cashed data. void dispose() { _resetCachedFrameData(); @@ -225,8 +228,8 @@ class InstabugScreenRenderManager { 1 / displayRefreshRate * 1000; /// Get device refresh rate from native side. - Future get _getDeviceRefreshRateFromNative => - APM.getDeviceRefreshRate(); + Future> get _getDeviceRefreshRateAndToleranceFromNative => + APM.getDeviceRefreshRateAndTolerance(); /// add new [WidgetsBindingObserver] to track app lifecycle. void _addWidgetBindingObserver() { @@ -260,12 +263,26 @@ class InstabugScreenRenderManager { analyzeFrameTiming(frameTiming); } }; - _deviceRefreshRate = await _getDeviceRefreshRateFromNative; - _slowFrameThresholdMs = _targetMsPerFrame(_deviceRefreshRate); + _slowFrameThresholdMs = await _getSlowFrameThresholdMs; _screenRenderForAutoUiTrace = InstabugScreenRenderData(frameData: []); _screenRenderForCustomUiTrace = InstabugScreenRenderData(frameData: []); } + Future get _getSlowFrameThresholdMs async { + final deviceRefreshRateAndTolerance = + await _getDeviceRefreshRateAndToleranceFromNative; + final deviceRefreshRate = deviceRefreshRateAndTolerance[0] ?? + 60; // default to 60 FPS if not available + final toleranceMs = (deviceRefreshRateAndTolerance[1] ?? 10) / + 1000; // convert the tolerance from microseconds to milliseconds + final targetMsPerFrame = _targetMsPerFrame(deviceRefreshRate); + return double.parse( + (targetMsPerFrame + toleranceMs).toStringAsFixed( + 2, + ), + ); // round the result to the nearest 2 precision digits + } + int _getEpochOffset(FrameTiming firstPatchedFrameTiming) { return DateTime.now().microsecondsSinceEpoch - firstPatchedFrameTiming.timestampInMicroseconds(FramePhase.vsyncStart); @@ -315,6 +332,7 @@ class InstabugScreenRenderManager { InstabugScreenRenderData screenRenderData, ) async { try { + screenRenderData.saveEndTime(); log( "reportScreenRenderForCustomUiTrace $screenRenderData", name: tag, 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 5a27d15f0..3c18b1642 100644 --- a/lib/src/utils/screen_rendering/instabug_widget_binding_observer.dart +++ b/lib/src/utils/screen_rendering/instabug_widget_binding_observer.dart @@ -36,6 +36,7 @@ class InstabugWidgetsBindingObserver extends WidgetsBindingObserver { .then((uiTraceId) { if (uiTraceId != null && InstabugScreenRenderManager.I.screenRenderEnabled) { + InstabugScreenRenderManager.I.endScreenRenderCollector(); InstabugScreenRenderManager.I .startScreenRenderCollectorForTraceId(uiTraceId); } diff --git a/pigeons/apm.api.dart b/pigeons/apm.api.dart index 3711d53fc..eb7201792 100644 --- a/pigeons/apm.api.dart +++ b/pigeons/apm.api.dart @@ -3,42 +3,20 @@ import 'package:pigeon/pigeon.dart'; @HostApi() abstract class ApmHostApi { void setEnabled(bool isEnabled); - @async bool isEnabled(); - void setScreenLoadingEnabled(bool isEnabled); - @async bool isScreenLoadingEnabled(); - void setColdAppLaunchEnabled(bool isEnabled); - void setAutoUITraceEnabled(bool isEnabled); - @async - String? startExecutionTrace(String id, String name); - void startFlow(String name); - void setFlowAttribute(String name, String key, String? value); - void endFlow(String name); - - void setExecutionTraceAttribute( - String id, - String key, - String value, - ); - - void endExecutionTrace(String id); - void startUITrace(String name); - void endUITrace(); - void endAppLaunch(); - void networkLogAndroid(Map data); void startCpUiTrace(String screenName, int microTimeStamp, int traceId); @@ -58,7 +36,7 @@ abstract class ApmHostApi { bool isScreenRenderEnabled(); @async - double deviceRefreshRate(); + List getDeviceRefreshRateAndTolerance(); void setScreenRenderEnabled(bool isEnabled); diff --git a/pigeons/instabug.api.dart b/pigeons/instabug.api.dart index 275306987..9ae724a66 100644 --- a/pigeons/instabug.api.dart +++ b/pigeons/instabug.api.dart @@ -37,9 +37,6 @@ abstract class InstabugHostApi { @async List? getTags(); - void addExperiments(List experiments); - void removeExperiments(List experiments); - void clearAllExperiments(); void addFeatureFlags(Map featureFlagsMap); void removeFeatureFlags(List featureFlags); void removeAllFeatureFlags(); @@ -76,4 +73,6 @@ abstract class InstabugHostApi { void willRedirectToStore(); void setNetworkLogBodyEnabled(bool isEnabled); + + void setTheme(Map themeConfig); } diff --git a/test/apm_test.dart b/test/apm_test.dart index 76aaf28f4..3ca288f68 100644 --- a/test/apm_test.dart +++ b/test/apm_test.dart @@ -89,48 +89,6 @@ void main() { ).called(1); }); - test('[startExecutionTrace] should call host method', () async { - final id = DateTime.now(); - const name = "trace"; - - when(mDateTime.now()).thenAnswer((_) => id); - when(mHost.startExecutionTrace(id.toString(), name)) - .thenAnswer((_) async => id.toString()); - - // ignore: deprecated_member_use_from_same_package - final trace = await APM.startExecutionTrace(name); - - expect(trace.id, id.toString()); - - verify( - mHost.startExecutionTrace(id.toString(), name), - ).called(1); - }); - - test('[setExecutionTraceAttribute] should call host method', () async { - final id = DateTime.now().toString(); - const key = "attr-key"; - const attribute = "Trace Attribute"; - - // ignore: deprecated_member_use_from_same_package - await APM.setExecutionTraceAttribute(id, key, attribute); - - verify( - mHost.setExecutionTraceAttribute(id, key, attribute), - ).called(1); - }); - - test('[endExecutionTrace] should call host method', () async { - final id = DateTime.now().toString(); - - // ignore: deprecated_member_use_from_same_package - await APM.endExecutionTrace(id); - - verify( - mHost.endExecutionTrace(id), - ).called(1); - }); - test('[startFlow] should call host method', () async { const flowName = "flow-name"; await APM.startFlow(flowName); @@ -275,10 +233,13 @@ void main() { verify(mHost.isScreenRenderEnabled()); }); - test("[getDeviceRefreshRate] should call host method", () async { - when(mHost.deviceRefreshRate()).thenAnswer((_) async => 60.0); - await APM.getDeviceRefreshRate(); - verify(mHost.deviceRefreshRate()).called(1); + test("[getDeviceRefreshRateAndTolerance] should call host method", + () async { + when(mHost.getDeviceRefreshRateAndTolerance()).thenAnswer( + (_) async => [60.0, 10.0], + ); + await APM.getDeviceRefreshRateAndTolerance(); + verify(mHost.getDeviceRefreshRateAndTolerance()).called(1); }); test("[setScreenRenderEnabled] should call host method", () async { @@ -349,7 +310,7 @@ void main() { await APM.endUITrace(); verify( - mScreenRenderManager.endScreenRenderCollector(), + mScreenRenderManager.endScreenRenderCollector(UiTraceType.custom), ).called(1); verifyNever(mHost.endUITrace()); }); diff --git a/test/instabug_test.dart b/test/instabug_test.dart index 06f3a3ef2..dee02c49f 100644 --- a/test/instabug_test.dart +++ b/test/instabug_test.dart @@ -206,16 +206,6 @@ void main() { ).called(1); }); - test('[setPrimaryColor] should call host method', () async { - const color = Color(0x00000000); - - await Instabug.setPrimaryColor(color); - - verify( - mHost.setPrimaryColor(color.value), - ).called(1); - }); - test('[setSessionProfilerEnabled] should call host method', () async { const enabled = true; @@ -267,37 +257,6 @@ void main() { ).called(1); }); - test('[addExperiments] should call host method', () async { - const experiments = ["exp-1", "exp-2"]; - - // ignore: deprecated_member_use_from_same_package - await Instabug.addExperiments(experiments); - - verify( - mHost.addExperiments(experiments), - ).called(1); - }); - - test('[removeExperiments] should call host method', () async { - const experiments = ["exp-1", "exp-2"]; - - // ignore: deprecated_member_use_from_same_package - await Instabug.removeExperiments(experiments); - - verify( - mHost.removeExperiments(experiments), - ).called(1); - }); - - test('[clearAllExperiments] should call host method', () async { - // ignore: deprecated_member_use_from_same_package - await Instabug.clearAllExperiments(); - - verify( - mHost.clearAllExperiments(), - ).called(1); - }); - test('[addFeatureFlags] should call host method', () async { await Instabug.addFeatureFlags([ FeatureFlag(name: 'name1', variant: 'variant1'), @@ -483,4 +442,14 @@ void main() { mHost.willRedirectToStore(), ).called(1); }); + + test('[setTheme] should call host method with theme config', () async { + const themeConfig = ThemeConfig(primaryColor: '#FF0000'); + + await Instabug.setTheme(themeConfig); + + verify( + mHost.setTheme(themeConfig.toMap()), + ).called(1); + }); } diff --git a/test/trace_test.dart b/test/trace_test.dart deleted file mode 100644 index 2415420be..000000000 --- a/test/trace_test.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:instabug_flutter/instabug_flutter.dart'; -import 'package:instabug_flutter/src/generated/apm.api.g.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -import 'trace_test.mocks.dart'; - -@GenerateMocks([ - ApmHostApi, -]) -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - WidgetsFlutterBinding.ensureInitialized(); - - final mHost = MockApmHostApi(); - final trace = Trace( - id: "trace", - name: "Execution Trace", - ); - - setUpAll(() { - APM.$setHostApi(mHost); - }); - - test('[end] should call host method', () async { - // ignore: deprecated_member_use_from_same_package - trace.end(); - - verify( - mHost.endExecutionTrace(trace.id), - ).called(1); - }); - - test('[setAttribute] should call host method', () async { - const key = "attr-key"; - const attribute = "Trace Attribute"; - // ignore: deprecated_member_use_from_same_package - trace.setAttribute(key, attribute); - - verify( - mHost.setExecutionTraceAttribute(trace.id, key, attribute), - ).called(1); - }); -} diff --git a/test/utils/screen_render/instabug_screen_render_manager_test.dart b/test/utils/screen_render/instabug_screen_render_manager_test.dart index adbe7d74d..22a1a710f 100644 --- a/test/utils/screen_render/instabug_screen_render_manager_test.dart +++ b/test/utils/screen_render/instabug_screen_render_manager_test.dart @@ -5,7 +5,7 @@ import 'package:instabug_flutter/src/models/instabug_screen_render_data.dart'; import 'package:instabug_flutter/src/utils/screen_rendering/instabug_screen_render_manager.dart'; import 'package:mockito/mockito.dart'; -import 'instabug_screen_render_manager_test_manual_mocks.dart'; +import 'instabug_screen_render_manager_test_mocks.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -19,7 +19,8 @@ void main() { mWidgetBinding = MockWidgetsBinding(); manager = InstabugScreenRenderManager.init(); // test-only constructor APM.$setHostApi(mApmHost); - when(mApmHost.deviceRefreshRate()).thenAnswer((_) async => 60); + when(mApmHost.getDeviceRefreshRateAndTolerance()) + .thenAnswer((_) async => [60, 0]); manager.init(mWidgetBinding); }); @@ -44,6 +45,38 @@ void main() { }); }); + // group('_initStaticValues', () { + // + // test('should initialize _timingsCallback', () async { + // await manager.callInitStaticValues(); + // expect(manager.timingsCallback, isNotNull); + // }); + // + // test('should initialize _slowFrameThresholdMs with value from _getSlowFrameThresholdMs', () async { + // // Patch the getter to return a known value + // manager.slowFrameThresholdMs = 0.0; + // manager.getSlowFrameThresholdMsFuture = () async => 42.0; + // await manager.callInitStaticValues(); + // expect(manager.slowFrameThresholdMs, 42.0); + // }); + // + // test('should initialize _screenRenderForAutoUiTrace and _screenRenderForCustomUiTrace as empty InstabugScreenRenderData', () async { + // await manager.callInitStaticValues(); + // expect(manager.screenRenderForAutoUiTrace, isA()); + // expect(manager.screenRenderForAutoUiTrace.frameData, isEmpty); + // expect(manager.screenRenderForCustomUiTrace, isA()); + // expect(manager.screenRenderForCustomUiTrace.frameData, isEmpty); + // }); + // + // test('should set _epochOffset on first timing in _timingsCallback', () async { + // await manager.callInitStaticValues(); + // final mockFrameTiming = MockFrameTiming(); + // manager.epochOffset = null; + // manager.timingsCallback([mockFrameTiming]); + // expect(manager.epochOffset, isNotNull); + // }); + // }); + group('startScreenRenderCollectorForTraceId()', () { test('should not attach timing listener if it is attached', () async { manager.startScreenRenderCollectorForTraceId(1); @@ -130,15 +163,11 @@ void main() { }); test( - 'should save and data to screenRenderForAutoUiTrace when for autoUITrace', + 'for auto UITrace should report data to native using endScreenRenderForAutoUiTrace', () { final frameTestData = InstabugScreenRenderData( traceId: 123, - frameData: [ - InstabugFrameData(10000, 400), - InstabugFrameData(10000, 600), - InstabugFrameData(20000, 1000), - ], + frameData: [], frozenFramesTotalDurationMicro: 1000, slowFramesTotalDurationMicro: 1000, endTimeMicro: 30000, @@ -148,23 +177,28 @@ void main() { frameTestData.traceId, ); + manager.startScreenRenderCollectorForTraceId( + frameTestData.traceId + 1, + UiTraceType.custom, + ); + manager.setFrameData(frameTestData); manager.stopScreenRenderCollector(); - expect(manager.screenRenderForAutoUiTrace.isActive, true); - - expect(manager.screenRenderForCustomUiTrace.isActive, false); - - expect(manager.screenRenderForAutoUiTrace == frameTestData, true); - verify( mApmHost.endScreenRenderForAutoUiTrace(any), ); // the content has been verified in the above assertion. + + expect(manager.screenRenderForAutoUiTrace.isActive, false); + + expect(manager.screenRenderForCustomUiTrace.isActive, false); + + expect(manager.screenRenderForAutoUiTrace.isEmpty, true); }); test( - 'should save and data to screenRenderForCustomUiTrace when for customUITrace', + 'for custom UITrace should report data to native using endScreenRenderForCustomUiTrace', () { final frameTestData = InstabugScreenRenderData( traceId: 123, @@ -187,11 +221,11 @@ void main() { manager.stopScreenRenderCollector(); - expect(manager.screenRenderForCustomUiTrace.isActive, true); + expect(manager.screenRenderForCustomUiTrace.isActive, false); expect(manager.screenRenderForAutoUiTrace.isActive, false); - expect(manager.screenRenderForCustomUiTrace == frameTestData, true); + expect(manager.screenRenderForCustomUiTrace.isEmpty, true); verify( mApmHost.endScreenRenderForCustomUiTrace(any), @@ -329,15 +363,10 @@ void main() { manager.analyzeFrameTiming(mockFrameTiming); // mock frame timing manager.stopScreenRenderCollector(); // should save data - expect(manager.screenRenderForAutoUiTrace.frameData.length, 1); - expect( - manager.screenRenderForAutoUiTrace.slowFramesTotalDurationMicro, - buildDuration * 1000, - ); // * 1000 to convert from milliseconds to microseconds expect( - manager.screenRenderForAutoUiTrace.frozenFramesTotalDurationMicro, - 0, - ); + manager.screenRenderForAutoUiTrace.frameData.isEmpty, + true, + ); // reset cached data after sync }); test('should detect slow frame on raster thread and record duration', () { @@ -349,15 +378,10 @@ void main() { manager.analyzeFrameTiming(mockFrameTiming); // mock frame timing manager.stopScreenRenderCollector(); // should save data - expect(manager.screenRenderForAutoUiTrace.frameData.length, 1); - expect( - manager.screenRenderForAutoUiTrace.slowFramesTotalDurationMicro, - rasterDuration * 1000, - ); // * 1000 to convert from milliseconds to microseconds expect( - manager.screenRenderForAutoUiTrace.frozenFramesTotalDurationMicro, - 0, - ); + manager.screenRenderForAutoUiTrace.frameData.isEmpty, + true, + ); // reset cached data after sync }); test( @@ -370,15 +394,10 @@ void main() { manager.analyzeFrameTiming(mockFrameTiming); // mock frame timing manager.stopScreenRenderCollector(); // should save data - expect(manager.screenRenderForAutoUiTrace.frameData.length, 1); - expect( - manager.screenRenderForAutoUiTrace.frozenFramesTotalDurationMicro, - totalTime * 1000, - ); // * 1000 to convert from milliseconds to microseconds expect( - manager.screenRenderForAutoUiTrace.slowFramesTotalDurationMicro, - 0, - ); + manager.screenRenderForAutoUiTrace.frameData.isEmpty, + true, + ); // reset cached data after sync }); test('should detect no slow or frozen frame under thresholds', () { diff --git a/test/utils/screen_render/instabug_screen_render_manager_test_manual_mocks.dart b/test/utils/screen_render/instabug_screen_render_manager_test_mocks.dart similarity index 92% rename from test/utils/screen_render/instabug_screen_render_manager_test_manual_mocks.dart rename to test/utils/screen_render/instabug_screen_render_manager_test_mocks.dart index 64d147846..856450dd4 100644 --- a/test/utils/screen_render/instabug_screen_render_manager_test_manual_mocks.dart +++ b/test/utils/screen_render/instabug_screen_render_manager_test_mocks.dart @@ -27,32 +27,32 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types -class _FakeFocusManager_0 extends _i1.Fake implements _i2.FocusManager { +class _FakeDuration_0 extends _i1.Fake implements Duration {} + +class _FakeFocusManager_1 extends _i1.Fake implements _i2.FocusManager { @override String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => super.toString(); } -class _FakeSingletonFlutterWindow_1 extends _i1.Fake +class _FakeSingletonFlutterWindow_2 extends _i1.Fake implements _i4.SingletonFlutterWindow {} -class _FakePlatformDispatcher_2 extends _i1.Fake +class _FakePlatformDispatcher_3 extends _i1.Fake implements _i4.PlatformDispatcher {} -class _FakeHardwareKeyboard_3 extends _i1.Fake implements - _i5.HardwareKeyboard {} +class _FakeHardwareKeyboard_4 extends _i1.Fake + implements _i5.HardwareKeyboard {} -class _FakeKeyEventManager_4 extends _i1.Fake implements _i5.KeyEventManager {} +class _FakeKeyEventManager_5 extends _i1.Fake implements _i5.KeyEventManager {} -class _FakeBinaryMessenger_5 extends _i1.Fake implements _i5.BinaryMessenger {} +class _FakeBinaryMessenger_6 extends _i1.Fake implements _i5.BinaryMessenger {} -class _FakeChannelBuffers_6 extends _i1.Fake implements _i4.ChannelBuffers {} +class _FakeChannelBuffers_7 extends _i1.Fake implements _i4.ChannelBuffers {} -class _FakeRestorationManager_7 extends _i1.Fake +class _FakeRestorationManager_8 extends _i1.Fake implements _i5.RestorationManager {} -class _FakeDuration_8 extends _i1.Fake implements Duration {} - class _FakePointerRouter_9 extends _i1.Fake implements _i6.PointerRouter {} class _FakeGestureArenaManager_10 extends _i1.Fake @@ -119,11 +119,6 @@ class MockApmHostApi extends _i1.Mock implements _i8.ApmHostApi { returnValue: Future.value(), returnValueForMissingStub: Future.value()) as _i9.Future); @override - _i9.Future startExecutionTrace(String? arg_id, String? arg_name) => - (super.noSuchMethod( - Invocation.method(#startExecutionTrace, [arg_id, arg_name]), - returnValue: Future.value()) as _i9.Future); - @override _i9.Future startFlow(String? arg_name) => (super.noSuchMethod(Invocation.method(#startFlow, [arg_name]), returnValue: Future.value(), @@ -141,19 +136,6 @@ class MockApmHostApi extends _i1.Mock implements _i8.ApmHostApi { returnValue: Future.value(), returnValueForMissingStub: Future.value()) as _i9.Future); @override - _i9.Future setExecutionTraceAttribute( - String? arg_id, String? arg_key, String? arg_value) => - (super.noSuchMethod( - Invocation.method( - #setExecutionTraceAttribute, [arg_id, arg_key, arg_value]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); - @override - _i9.Future endExecutionTrace(String? arg_id) => - (super.noSuchMethod(Invocation.method(#endExecutionTrace, [arg_id]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i9.Future); - @override _i9.Future startUITrace(String? arg_name) => (super.noSuchMethod(Invocation.method(#startUITrace, [arg_name]), returnValue: Future.value(), @@ -206,9 +188,11 @@ class MockApmHostApi extends _i1.Mock implements _i8.ApmHostApi { (super.noSuchMethod(Invocation.method(#isScreenRenderEnabled, []), returnValue: Future.value(false)) as _i9.Future); @override - _i9.Future deviceRefreshRate() => - (super.noSuchMethod(Invocation.method(#deviceRefreshRate, []), - returnValue: Future.value(0.0)) as _i9.Future); + _i9.Future> getDeviceRefreshRateAndTolerance() => + (super.noSuchMethod( + Invocation.method(#getDeviceRefreshRateAndTolerance, []), + returnValue: Future>.value([])) + as _i9.Future>); @override _i9.Future setScreenRenderEnabled(bool? arg_isEnabled) => (super .noSuchMethod(Invocation.method(#setScreenRenderEnabled, [arg_isEnabled]), @@ -230,6 +214,63 @@ class MockApmHostApi extends _i1.Mock implements _i8.ApmHostApi { returnValueForMissingStub: Future.value()) as _i9.Future); } +/// A class which mocks [FrameTiming]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFrameTiming extends _i1.Mock implements _i4.FrameTiming { + MockFrameTiming() { + _i1.throwOnMissingStub(this); + } + + @override + Duration get buildDuration => + (super.noSuchMethod(Invocation.getter(#buildDuration), + returnValue: _FakeDuration_0()) as Duration); + @override + Duration get rasterDuration => + (super.noSuchMethod(Invocation.getter(#rasterDuration), + returnValue: _FakeDuration_0()) as Duration); + @override + Duration get vsyncOverhead => + (super.noSuchMethod(Invocation.getter(#vsyncOverhead), + returnValue: _FakeDuration_0()) as Duration); + @override + Duration get totalSpan => (super.noSuchMethod(Invocation.getter(#totalSpan), + returnValue: _FakeDuration_0()) as Duration); + @override + int get layerCacheCount => + (super.noSuchMethod(Invocation.getter(#layerCacheCount), returnValue: 0) + as int); + @override + int get layerCacheBytes => + (super.noSuchMethod(Invocation.getter(#layerCacheBytes), returnValue: 0) + as int); + @override + double get layerCacheMegabytes => + (super.noSuchMethod(Invocation.getter(#layerCacheMegabytes), + returnValue: 0.0) as double); + @override + int get pictureCacheCount => + (super.noSuchMethod(Invocation.getter(#pictureCacheCount), returnValue: 0) + as int); + @override + int get pictureCacheBytes => + (super.noSuchMethod(Invocation.getter(#pictureCacheBytes), returnValue: 0) + as int); + @override + double get pictureCacheMegabytes => + (super.noSuchMethod(Invocation.getter(#pictureCacheMegabytes), + returnValue: 0.0) as double); + @override + int get frameNumber => + (super.noSuchMethod(Invocation.getter(#frameNumber), returnValue: 0) + as int); + @override + int timestampInMicroseconds(_i4.FramePhase? phase) => + (super.noSuchMethod(Invocation.method(#timestampInMicroseconds, [phase]), + returnValue: 0) as int); +} + /// A class which mocks [WidgetsBinding]. /// /// See the documentation for Mockito's code generation for more information. @@ -251,7 +292,7 @@ class MockWidgetsBinding extends _i1.Mock implements _i10.WidgetsBinding { @override _i2.FocusManager get focusManager => (super.noSuchMethod(Invocation.getter(#focusManager), - returnValue: _FakeFocusManager_0()) as _i2.FocusManager); + returnValue: _FakeFocusManager_1()) as _i2.FocusManager); @override bool get firstFrameRasterized => (super.noSuchMethod(Invocation.getter(#firstFrameRasterized), @@ -275,12 +316,12 @@ class MockWidgetsBinding extends _i1.Mock implements _i10.WidgetsBinding { @override _i4.SingletonFlutterWindow get window => (super.noSuchMethod(Invocation.getter(#window), - returnValue: _FakeSingletonFlutterWindow_1()) + returnValue: _FakeSingletonFlutterWindow_2()) as _i4.SingletonFlutterWindow); @override _i4.PlatformDispatcher get platformDispatcher => (super.noSuchMethod(Invocation.getter(#platformDispatcher), - returnValue: _FakePlatformDispatcher_2()) as _i4.PlatformDispatcher); + returnValue: _FakePlatformDispatcher_3()) as _i4.PlatformDispatcher); @override bool get locked => (super.noSuchMethod(Invocation.getter(#locked), returnValue: false) @@ -288,23 +329,23 @@ class MockWidgetsBinding extends _i1.Mock implements _i10.WidgetsBinding { @override _i5.HardwareKeyboard get keyboard => (super.noSuchMethod(Invocation.getter(#keyboard), - returnValue: _FakeHardwareKeyboard_3()) as _i5.HardwareKeyboard); + returnValue: _FakeHardwareKeyboard_4()) as _i5.HardwareKeyboard); @override _i5.KeyEventManager get keyEventManager => (super.noSuchMethod(Invocation.getter(#keyEventManager), - returnValue: _FakeKeyEventManager_4()) as _i5.KeyEventManager); + returnValue: _FakeKeyEventManager_5()) as _i5.KeyEventManager); @override _i5.BinaryMessenger get defaultBinaryMessenger => (super.noSuchMethod(Invocation.getter(#defaultBinaryMessenger), - returnValue: _FakeBinaryMessenger_5()) as _i5.BinaryMessenger); + returnValue: _FakeBinaryMessenger_6()) as _i5.BinaryMessenger); @override _i4.ChannelBuffers get channelBuffers => (super.noSuchMethod(Invocation.getter(#channelBuffers), - returnValue: _FakeChannelBuffers_6()) as _i4.ChannelBuffers); + returnValue: _FakeChannelBuffers_7()) as _i4.ChannelBuffers); @override _i5.RestorationManager get restorationManager => (super.noSuchMethod(Invocation.getter(#restorationManager), - returnValue: _FakeRestorationManager_7()) as _i5.RestorationManager); + returnValue: _FakeRestorationManager_8()) as _i5.RestorationManager); @override _i11.SchedulingStrategy get schedulingStrategy => (super.noSuchMethod(Invocation.getter(#schedulingStrategy), @@ -333,11 +374,11 @@ class MockWidgetsBinding extends _i1.Mock implements _i10.WidgetsBinding { @override Duration get currentFrameTimeStamp => (super.noSuchMethod(Invocation.getter(#currentFrameTimeStamp), - returnValue: _FakeDuration_8()) as Duration); + returnValue: _FakeDuration_0()) as Duration); @override Duration get currentSystemFrameTimeStamp => (super.noSuchMethod(Invocation.getter(#currentSystemFrameTimeStamp), - returnValue: _FakeDuration_8()) as Duration); + returnValue: _FakeDuration_0()) as Duration); @override _i6.PointerRouter get pointerRouter => (super.noSuchMethod(Invocation.getter(#pointerRouter), @@ -362,7 +403,7 @@ class MockWidgetsBinding extends _i1.Mock implements _i10.WidgetsBinding { @override Duration get samplingOffset => (super.noSuchMethod(Invocation.getter(#samplingOffset), - returnValue: _FakeDuration_8()) as Duration); + returnValue: _FakeDuration_0()) as Duration); @override set samplingOffset(Duration? _samplingOffset) => super.noSuchMethod(Invocation.setter(#samplingOffset, _samplingOffset), @@ -540,7 +581,7 @@ class MockWidgetsBinding extends _i1.Mock implements _i10.WidgetsBinding { @override _i5.BinaryMessenger createBinaryMessenger() => (super.noSuchMethod(Invocation.method(#createBinaryMessenger, []), - returnValue: _FakeBinaryMessenger_5()) as _i5.BinaryMessenger); + returnValue: _FakeBinaryMessenger_6()) as _i5.BinaryMessenger); @override _i9.Future handleSystemMessage(Object? systemMessage) => (super .noSuchMethod(Invocation.method(#handleSystemMessage, [systemMessage]), @@ -561,7 +602,7 @@ class MockWidgetsBinding extends _i1.Mock implements _i10.WidgetsBinding { @override _i5.RestorationManager createRestorationManager() => (super.noSuchMethod(Invocation.method(#createRestorationManager, []), - returnValue: _FakeRestorationManager_7()) as _i5.RestorationManager); + returnValue: _FakeRestorationManager_8()) as _i5.RestorationManager); @override void setSystemUiChangeCallback(_i5.SystemUiChangeCallback? callback) => super .noSuchMethod(Invocation.method(#setSystemUiChangeCallback, [callback]), @@ -700,60 +741,3 @@ class MockWidgetsBinding extends _i1.Mock implements _i10.WidgetsBinding { returnValue: _FakeSemanticsUpdateBuilder_17()) as _i4.SemanticsUpdateBuilder); } - -/// A class which mocks [FrameTiming]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockFrameTiming extends _i1.Mock implements _i4.FrameTiming { - MockFrameTiming() { - _i1.throwOnMissingStub(this); - } - - @override - Duration get buildDuration => - (super.noSuchMethod(Invocation.getter(#buildDuration), - returnValue: _FakeDuration_8()) as Duration); - @override - Duration get rasterDuration => - (super.noSuchMethod(Invocation.getter(#rasterDuration), - returnValue: _FakeDuration_8()) as Duration); - @override - Duration get vsyncOverhead => - (super.noSuchMethod(Invocation.getter(#vsyncOverhead), - returnValue: _FakeDuration_8()) as Duration); - @override - Duration get totalSpan => (super.noSuchMethod(Invocation.getter(#totalSpan), - returnValue: _FakeDuration_8()) as Duration); - @override - int get layerCacheCount => - (super.noSuchMethod(Invocation.getter(#layerCacheCount), returnValue: 0) - as int); - @override - int get layerCacheBytes => - (super.noSuchMethod(Invocation.getter(#layerCacheBytes), returnValue: 0) - as int); - @override - double get layerCacheMegabytes => - (super.noSuchMethod(Invocation.getter(#layerCacheMegabytes), - returnValue: 0.0) as double); - @override - int get pictureCacheCount => - (super.noSuchMethod(Invocation.getter(#pictureCacheCount), returnValue: 0) - as int); - @override - int get pictureCacheBytes => - (super.noSuchMethod(Invocation.getter(#pictureCacheBytes), returnValue: 0) - as int); - @override - double get pictureCacheMegabytes => - (super.noSuchMethod(Invocation.getter(#pictureCacheMegabytes), - returnValue: 0.0) as double); - @override - int get frameNumber => - (super.noSuchMethod(Invocation.getter(#frameNumber), returnValue: 0) - as int); - @override - int timestampInMicroseconds(_i4.FramePhase? phase) => - (super.noSuchMethod(Invocation.method(#timestampInMicroseconds, [phase]), - returnValue: 0) as int); -}