diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/launch/LaunchActivity.kt b/app/src/main/kotlin/io/homeassistant/companion/android/launch/LaunchActivity.kt index fbb21f35429..d0be92affa4 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/launch/LaunchActivity.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/launch/LaunchActivity.kt @@ -18,25 +18,39 @@ import androidx.compose.ui.res.stringResource import androidx.core.content.IntentCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.withCreationCallback +import io.homeassistant.companion.android.WIPFeature import io.homeassistant.companion.android.common.R as commonR import io.homeassistant.companion.android.common.compose.theme.HATheme +import io.homeassistant.companion.android.sensors.SensorReceiver +import io.homeassistant.companion.android.sensors.SensorWorker import io.homeassistant.companion.android.util.PLAY_SERVICES_FLAVOR_DOC_URL import io.homeassistant.companion.android.util.PlayServicesAvailability import io.homeassistant.companion.android.util.compose.HAApp import io.homeassistant.companion.android.util.compose.navigateToUri import io.homeassistant.companion.android.util.enableEdgeToEdgeCompat +import io.homeassistant.companion.android.websocket.WebsocketManager import javax.inject.Inject +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize private const val DEEP_LINK_KEY = "deep_link_key" /** - * Main entry point of the application, it is mostly responsible to hold the whole navigation of the application. + * Main entry point of the application, responsible for holding the whole navigation graph + * and triggering lifecycle-based refresh of background work. + * * It also handles the splash screen display based on a condition exposed by the [LaunchViewModel]. + * + * On resume, refreshes the scheduling of periodic sensor collection via [SensorWorker] + * and the background WebSocket work via [WebsocketManager]. + * These jobs are managed outside the Activity and may continue beyond this lifecycle. + * On pause, triggers an immediate sensor update via [SensorReceiver] so the server + * has fresh data before the app goes to the background. */ @AndroidEntryPoint class LaunchActivity : AppCompatActivity() { @@ -130,6 +144,21 @@ class LaunchActivity : AppCompatActivity() { } } } + + override fun onResume() { + super.onResume() + if (WIPFeature.USE_FRONTEND_V2) { + SensorWorker.start(this) + lifecycleScope.launch { + WebsocketManager.start(this@LaunchActivity) + } + } + } + + override fun onPause() { + super.onPause() + if (!isFinishing && WIPFeature.USE_FRONTEND_V2) SensorReceiver.updateAllSensors(this) + } } @Composable diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/launch/LaunchActivityTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/launch/LaunchActivityTest.kt new file mode 100644 index 00000000000..693b1c7398b --- /dev/null +++ b/app/src/test/kotlin/io/homeassistant/companion/android/launch/LaunchActivityTest.kt @@ -0,0 +1,86 @@ +package io.homeassistant.companion.android.launch + +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.work.testing.WorkManagerTestInitHelper +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.HiltTestApplication +import dagger.hilt.android.testing.UninstallModules +import io.homeassistant.companion.android.common.data.servers.ServerManager +import io.homeassistant.companion.android.di.ServerManagerModule +import io.homeassistant.companion.android.sensors.SensorReceiver +import io.homeassistant.companion.android.sensors.SensorWorker +import io.homeassistant.companion.android.testing.unit.ConsoleLogRule +import io.homeassistant.companion.android.websocket.WebsocketManager +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject +import io.mockk.verify +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(application = HiltTestApplication::class) +@UninstallModules(ServerManagerModule::class) +@HiltAndroidTest +class LaunchActivityTest { + + @get:Rule(order = 0) + val consoleLogRule = ConsoleLogRule() + + @get:Rule(order = 1) + val hiltRule = HiltAndroidRule(this) + + @BindValue + @JvmField + val serverManager: ServerManager = mockk(relaxed = true) { + coEvery { getServer(any()) } returns null + } + + @Before + fun setUp() { + WorkManagerTestInitHelper.initializeTestWorkManager(ApplicationProvider.getApplicationContext()) + mockkObject(SensorWorker.Companion) + mockkObject(WebsocketManager.Companion) + mockkObject(SensorReceiver.Companion) + every { SensorWorker.start(any()) } just Runs + coEvery { WebsocketManager.start(any()) } just Runs + every { SensorReceiver.updateAllSensors(any()) } just Runs + } + + @After + fun tearDown() { + unmockkObject(SensorWorker.Companion) + unmockkObject(WebsocketManager.Companion) + unmockkObject(SensorReceiver.Companion) + } + + @Test + fun `Given activity resumes then sensor worker and websocket manager are started`() { + ActivityScenario.launch(LaunchActivity::class.java).use { + verify { SensorWorker.start(any()) } + coVerify { WebsocketManager.start(any()) } + } + } + + @Test + fun `Given activity pauses without finishing then all sensors are updated`() { + ActivityScenario.launch(LaunchActivity::class.java).use { scenario -> + scenario.moveToState(Lifecycle.State.STARTED) // triggers onPause + verify { SensorReceiver.updateAllSensors(any()) } + } + } +}