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/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;