Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Int>()) } 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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes no sense to me, and I wouldn't be surprised if it turns out to be flaky. The documentation mentions (including emphasis!):

Started state for a LifecycleOwner. For an android.app.Activity, this state is reached in two cases:

  • after android.app.Activity.onStart call;
  • right before android.app.Activity.onPause call.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's for the test library
image

for me when calling moveToState with STARTED it does invoke pauseActivity
image

I didn't see any flakiness on this test, I did run it multiple times without any issue.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the Google inconsistency... /s

In that case OK, no objection and it seems like the right way to test this.

verify { SensorReceiver.updateAllSensors(any()) }
}
}
}
Loading