diff --git a/CHANGELOG.md b/CHANGELOG.md index 30c13b81f..71831fe20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 24.7.2 +* Mitigated an issue in the upload plugin that prevented the upload of a symbol file + ## 24.7.1 * ! Minor breaking change ! Unsupported types for user properties will now be omitted, they won't be converted to strings. diff --git a/gradle.properties b/gradle.properties index db012571f..9e00f0cab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,7 +22,7 @@ org.gradle.configureondemand=true android.useAndroidX=true android.enableJetifier=true # RELEASE FIELD SECTION -VERSION_NAME=24.7.1 +VERSION_NAME=24.7.2 GROUP=ly.count.android POM_URL=https://github.com/Countly/countly-sdk-android POM_SCM_URL=https://github.com/Countly/countly-sdk-android diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/CountlyTimerTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/CountlyTimerTests.java new file mode 100644 index 000000000..44aa5d83b --- /dev/null +++ b/sdk/src/androidTest/java/ly/count/android/sdk/CountlyTimerTests.java @@ -0,0 +1,104 @@ +package ly.count.android.sdk; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.concurrent.ExecutorService; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +@RunWith(AndroidJUnit4.class) +public class CountlyTimerTests { + + private CountlyTimer countlyTimer; + private ModuleLog mockLog; + + @Before + public void setUp() { + countlyTimer = new CountlyTimer(); + mockLog = Mockito.mock(ModuleLog.class); + CountlyTimer.TIMER_DELAY_MS = 0; + } + + @After + public void tearDown() { + countlyTimer.stopTimer(mockLog); + Assert.assertNull(countlyTimer.timerService); + } + + @Test + public void validateInitialValues() { + Assert.assertNull(countlyTimer.timerService); + Assert.assertEquals(0, CountlyTimer.TIMER_DELAY_MS); + } + + @Test + public void startTimer_validDelay() { + Runnable mockRunnable = Mockito.mock(Runnable.class); + + countlyTimer.startTimer(1, mockRunnable, mockLog); + Mockito.verify(mockLog).i("[CountlyTimer] startTimer, Starting timer timerDelay: [1000 ms]"); + } + + @Test + public void startTimer_invalidDelay() { + Runnable mockRunnable = Mockito.mock(Runnable.class); + + countlyTimer.startTimer(-1, mockRunnable, mockLog); + Mockito.verify(mockLog).i("[CountlyTimer] startTimer, Starting timer timerDelay: [1000 ms]"); + } + + @Test + public void startTimer() { + Runnable mockRunnable = Mockito.mock(Runnable.class); + + countlyTimer.startTimer(99, mockRunnable, mockLog); + Mockito.verify(mockLog).i("[CountlyTimer] startTimer, Starting timer timerDelay: [99000 ms]"); + } + + @Test + public void startTimer_withTimerDelayMS() { + CountlyTimer.TIMER_DELAY_MS = 500; + Runnable mockRunnable = Mockito.mock(Runnable.class); + + countlyTimer.startTimer(1, mockRunnable, mockLog); + Mockito.verify(mockLog).i("[CountlyTimer] startTimer, Starting timer timerDelay: [500 ms]"); + } + + /** + * Test that the timer is stopped when a new timer is started + * This is to prevent multiple timers from running at the same time + * And it is not reusing the previous timer + */ + @Test + public void startTimer_reuseTimer() { + countlyTimer.stopTimer(mockLog); + + Assert.assertNull(countlyTimer.timerService); + + Runnable mockRunnable = Mockito.mock(Runnable.class); + countlyTimer.startTimer(1, mockRunnable, mockLog); + + Assert.assertNotNull(countlyTimer.timerService); + ExecutorService timerService = countlyTimer.timerService; + + countlyTimer.startTimer(2, mockRunnable, mockLog); + Assert.assertNotEquals(timerService, countlyTimer.timerService); + } + + @Test + public void stopTimer() { + countlyTimer.startTimer(1, Mockito.mock(Runnable.class), mockLog); + countlyTimer.stopTimer(mockLog); + Mockito.verify(mockLog).i("[CountlyTimer] stopTimer, Stopping timer"); + } + + @Test + public void stopTimer_nullTimer() { + countlyTimer.stopTimer(mockLog); + Mockito.verify(mockLog, Mockito.never()).i("[CountlyTimer] stopTimer, Stopping timer"); + Mockito.verify(mockLog).d("[CountlyTimer] stopTimer, Timer already stopped"); + } +} diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java b/sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java index d317e9d8b..af6cce718 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java @@ -42,7 +42,7 @@ public class TestUtils { public final static String commonAppKey = "appkey"; public final static String commonDeviceId = "1234"; public final static String SDK_NAME = "java-native-android"; - public final static String SDK_VERSION = "24.7.1"; + public final static String SDK_VERSION = "24.7.2"; public static final int MAX_THREAD_COUNT_PER_STACK_TRACE = 50; public static class Activity2 extends Activity { diff --git a/sdk/src/main/java/ly/count/android/sdk/Countly.java b/sdk/src/main/java/ly/count/android/sdk/Countly.java index 3aa917d00..4dc0f8714 100644 --- a/sdk/src/main/java/ly/count/android/sdk/Countly.java +++ b/sdk/src/main/java/ly/count/android/sdk/Countly.java @@ -47,7 +47,7 @@ of this software and associated documentation files (the "Software"), to deal */ public class Countly { - private final String DEFAULT_COUNTLY_SDK_VERSION_STRING = "24.7.1"; + private final String DEFAULT_COUNTLY_SDK_VERSION_STRING = "24.7.2"; /** * Used as request meta data on every request diff --git a/sdk/src/main/java/ly/count/android/sdk/CountlyTimer.java b/sdk/src/main/java/ly/count/android/sdk/CountlyTimer.java new file mode 100644 index 000000000..bbb482a6e --- /dev/null +++ b/sdk/src/main/java/ly/count/android/sdk/CountlyTimer.java @@ -0,0 +1,54 @@ +package ly.count.android.sdk; + +import androidx.annotation.NonNull; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +class CountlyTimer { + + ScheduledExecutorService timerService; + protected static int TIMER_DELAY_MS = 0; // for testing purposes + + protected void stopTimer(@NonNull ModuleLog L) { + if (timerService != null) { + L.i("[CountlyTimer] stopTimer, Stopping timer"); + try { + timerService.shutdown(); + if (!timerService.awaitTermination(1, TimeUnit.SECONDS)) { + timerService.shutdownNow(); + if (!timerService.awaitTermination(1, TimeUnit.SECONDS)) { + L.e("[CountlyTimer] stopTimer, Global timer must be locked"); + } + } + } catch (Exception e) { + L.e("[CountlyTimer] stopTimer, Error while stopping global timer " + e); + } + timerService = null; + } else { + L.d("[CountlyTimer] stopTimer, Timer already stopped"); + } + } + + protected void startTimer(long timerDelay, @NonNull Runnable runnable, @NonNull ModuleLog L) { + long timerDelayInternal = timerDelay * 1000; + + if (timerDelayInternal < UtilsTime.ONE_SECOND_IN_MS) { + timerDelayInternal = UtilsTime.ONE_SECOND_IN_MS; + } + + if (TIMER_DELAY_MS > 0) { + timerDelayInternal = TIMER_DELAY_MS; + } + + L.i("[CountlyTimer] startTimer, Starting timer timerDelay: [" + timerDelayInternal + " ms]"); + + if (timerService != null) { + L.d("[CountlyTimer] startTimer, timer was running, stopping it"); + stopTimer(L); + } + + timerService = Executors.newSingleThreadScheduledExecutor(); + timerService.scheduleWithFixedDelay(runnable, 0, timerDelayInternal, TimeUnit.MILLISECONDS); + } +} diff --git a/sdk/src/main/java/ly/count/android/sdk/UtilsTime.java b/sdk/src/main/java/ly/count/android/sdk/UtilsTime.java index 07287948c..99d731f6e 100644 --- a/sdk/src/main/java/ly/count/android/sdk/UtilsTime.java +++ b/sdk/src/main/java/ly/count/android/sdk/UtilsTime.java @@ -6,6 +6,7 @@ import java.util.List; public class UtilsTime { + protected static int ONE_SECOND_IN_MS = 1000; public static class Instant { public final long timestampMs; diff --git a/upload-plugin/src/main/groovy/ly/count/android/plugins/uploadSymbols.groovy b/upload-plugin/src/main/groovy/ly/count/android/plugins/uploadSymbols.groovy index 72f073859..3e0cdb1b6 100644 --- a/upload-plugin/src/main/groovy/ly/count/android/plugins/uploadSymbols.groovy +++ b/upload-plugin/src/main/groovy/ly/count/android/plugins/uploadSymbols.groovy @@ -36,7 +36,10 @@ class UploadSymbolsPlugin implements Plugin { throw new StopExecutionException("Please specify your server in countly block.") } String buildVersion = project.android.defaultConfig.versionName - String url = "${ext.server}/i/crash_symbols/upload_symbol" + String url = ext.server; + String path = "i/crash_symbols/upload_symbol"; + // Ensure there is exactly one "/" between the base URL and the path + url = url.endsWith("/") ? url + path : url + "/" + path; def filePath = "$project.buildDir/$ext.mappingFile" logger.debug("uploadJavaSymbols, Version name:[ {} ], Upload symbol url:[ {} ], Mapping file path:[ {} ]", buildVersion, url, filePath) File file = new File(filePath) @@ -55,17 +58,27 @@ class UploadSymbolsPlugin implements Plugin { .build() request = new Request.Builder().url(url).post(formBody).build() } - logger.debug("uploadJavaSymbols, Generated request: {}", request.body().toString()) doLast { if (request == null) { logger.error("Request not constructed") throw new StopActionException("Something happened while constructing the request. Please try again.") } + + if (request.body() != null) { + logger.debug("uploadJavaSymbols, Generated request: {}", request.body().toString()) + } else { + logger.error("uploadJavaSymbols, Request body is null which should not be the case") + } + client = new OkHttpClient() Response response = client.newCall(request).execute() if (response.code() != 200) { - logger.error("An error occurred while uploading the mapping file: {}", response.body().string()) + if (response.body() != null) { + logger.error("An error occurred while uploading the mapping file: {}", response.body().string()) + } else { + logger.error("An error occurred while uploading the mapping file, response body null") + } } else { logger.debug("File upload successful") }