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
2 changes: 2 additions & 0 deletions feature/dashboard/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application>
<activity
android:name="com.simprints.feature.dashboard.DashboardActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ internal class TroubleshootingPagerAdapter(

// TODO Replace stub fragments with proper ones
Overview("Overview", { OverviewFragment() }),
Network("Network", { StubFragment() }),
Events("Events", { StubFragment() }),
Workers("Worker log", { StubFragment() }),
;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.simprints.feature.dashboard.R
import com.simprints.feature.dashboard.databinding.FragmentTroubleshootingOverviewBinding
import com.simprints.feature.dashboard.settings.troubleshooting.overview.usecase.PingServerUseCase.PingResult
import com.simprints.infra.uibase.viewbinding.viewBinding
import dagger.hilt.android.AndroidEntryPoint

Expand All @@ -24,8 +25,25 @@ class OverviewFragment : Fragment(R.layout.fragment_troubleshooting_overview) {
viewModel.licenseStates.observe(viewLifecycleOwner) {
binding.troubleshootOverviewLicences.text = it.ifBlank { "No licenses found" }
}
viewModel.networkStates.observe(viewLifecycleOwner) {
binding.troubleshootOverviewNetwork.text = it.orEmpty()
}

viewModel.collectData()

viewModel.pingResult.observe(viewLifecycleOwner) {
binding.troubleshootOverviewPingResult.text = when (it) {
PingResult.NotDone -> "Check not done yet"
PingResult.InProgress -> "Calling server"
is PingResult.Success -> "Success in ${it.message}"
is PingResult.Failure -> "Failed: ${it.message}"
}
binding.troubleshootOverviewPing.isEnabled = it != PingResult.InProgress
}
binding.troubleshootOverviewPing.setOnClickListener {
viewModel.pingServer()
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.simprints.feature.dashboard.settings.troubleshooting.overview.usecase.PingServerUseCase.PingResult
import com.simprints.feature.dashboard.settings.troubleshooting.overview.usecase.CollectIdsUseCase
import com.simprints.feature.dashboard.settings.troubleshooting.overview.usecase.CollectLicenceStatesUseCase
import com.simprints.feature.dashboard.settings.troubleshooting.overview.usecase.CollectNetworkInformationUseCase
import com.simprints.feature.dashboard.settings.troubleshooting.overview.usecase.PingServerUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject


@HiltViewModel
internal class OverviewViewModel @Inject constructor(
private val collectIds: CollectIdsUseCase,
private val collectLicenseStates: CollectLicenceStatesUseCase,
private val collectNetworkInformation: CollectNetworkInformationUseCase,
private val doServerPing: PingServerUseCase,
) : ViewModel() {

val projectIds: LiveData<String>
Expand All @@ -24,13 +30,23 @@ internal class OverviewViewModel @Inject constructor(
get() = _licenseStates
private val _licenseStates = MutableLiveData("")

val networkStates: LiveData<String>
get() = _networkStates
private val _networkStates = MutableLiveData("")

val pingResult: LiveData<PingResult>
get() = _pingResult
private val _pingResult = MutableLiveData<PingResult>(PingResult.NotDone)

fun collectData() {
_projectIds.postValue(collectIds())
viewModelScope.launch { _licenseStates.postValue(collectLicenseStates()) }
_networkStates.postValue(collectNetworkInformation())
}

fun pingServer() {
viewModelScope.launch {
_licenseStates.postValue(collectLicenseStates())
doServerPing().collect { _pingResult.postValue(it) }
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.simprints.feature.dashboard.settings.troubleshooting.overview.usecase

import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import com.simprints.infra.uibase.annotations.ExcludedFromGeneratedTestCoverageReports
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject

@ExcludedFromGeneratedTestCoverageReports("Mostly system calls")
class CollectNetworkInformationUseCase @Inject constructor(
@ApplicationContext private val context: Context,
) {

operator fun invoke(): String {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
?: return "No network capabilities available"

if (!networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
return "No internet connection"
}

val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
val connectionType = if (isWifi) "Connected to Wi-Fi" else "Connected to Cellular"
val connectionValidated = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)

return """
$connectionType
Strength: ${getSignalStrength(networkCapabilities)}
Internet access: $connectionValidated)}
Bandwidth up (kbps): ${networkCapabilities.linkUpstreamBandwidthKbps}
Bandwidth down (kbps): ${networkCapabilities.linkDownstreamBandwidthKbps}
""".trimIndent()
}

private fun getSignalStrength(networkCapabilities: NetworkCapabilities): String =
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) "N/A"
else {
val strength = networkCapabilities.signalStrength
val description = when {
strength >= -50 -> "Excellent"
strength >= -70 -> "Good"
strength >= -80 -> "Fair"
else -> "Weak"
}
"$strength ($description)"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.simprints.feature.dashboard.settings.troubleshooting.overview.usecase

import com.simprints.core.DispatcherIO
import com.simprints.infra.network.url.BaseUrlProvider
import com.simprints.infra.uibase.annotations.ExcludedFromGeneratedTestCoverageReports
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import java.net.HttpURLConnection
import java.net.URL
import javax.inject.Inject
import kotlin.time.measureTimedValue

internal class PingServerUseCase @Inject constructor(
@DispatcherIO private val dispatcherIO: CoroutineDispatcher,
private val baseUrlProvider: BaseUrlProvider,
private val urlFactory: UrlFactory,
) {

/**
* Does the most bare-bones version of HTTP request to access the root URL
* of the last configured BFSID server. Any response code below 500 is considered
* a success as it means that the server has been reached.
*/
operator fun invoke(): Flow<PingResult> = flow {
emit(PingResult.InProgress)

val urlToPing = urlFactory(baseUrlProvider.getApiBaseUrlPrefix())
val executionTime = measureTimedValue {
try {
val connection = urlToPing.openConnection().let { it as HttpURLConnection }
connection.connect()

val response = connection.responseCode
val result = if (response < HttpURLConnection.HTTP_INTERNAL_ERROR) "" else "$response"
connection.disconnect()

result
} catch (e: Throwable) {
e.message ?: e.localizedMessage ?: "Error during request"
}
}
if (executionTime.value.isEmpty()) {
emit(PingResult.Success("${executionTime.duration}"))
} else {
emit(PingResult.Failure(executionTime.value))
}
}.flowOn(dispatcherIO)

internal sealed class PingResult {

data object NotDone : PingResult()
data object InProgress : PingResult()
data class Success(val message: String) : PingResult()
data class Failure(val message: String) : PingResult()
}

@ExcludedFromGeneratedTestCoverageReports(
"Wrapper to allow substituting the URL constructor for testing purposes"
)
internal class UrlFactory @Inject constructor() {

operator fun invoke(baseUrl: String) = URL(baseUrl)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
style="@style/Text.Body1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.5"
android:lineSpacingMultiplier="1.25"
android:textIsSelectable="true"
tools:text="Device ID:\nProject ID:\nUser ID:" />

Expand All @@ -38,6 +38,46 @@
android:textIsSelectable="true"
tools:text="@tools:sample/lorem" />

<TextView
style="@style/Text.Headline6.Accented"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp"
android:lineSpacingMultiplier="1.5"
android:text="Network"
android:textIsSelectable="true" />

<TextView
android:id="@+id/troubleshootOverviewNetwork"
style="@style/Text.Body1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.25"
android:textIsSelectable="true"
tools:text="@tools:sample/lorem" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="16dp"
android:orientation="horizontal">

<com.google.android.material.button.MaterialButton
android:id="@+id/troubleshootOverviewPing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Check connection" />

<TextView
android:id="@+id/troubleshootOverviewPingResult"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:padding="8dp"
android:text="No checks done" />
</LinearLayout>

</LinearLayout>

</ScrollView>
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ package com.simprints.feature.dashboard.settings.troubleshooting.overview
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.google.common.truth.Truth.assertThat
import com.jraska.livedata.test
import com.simprints.core.domain.tokenization.TokenizableString
import com.simprints.feature.dashboard.settings.troubleshooting.overview.usecase.CollectIdsUseCase
import com.simprints.feature.dashboard.settings.troubleshooting.overview.usecase.CollectLicenceStatesUseCase
import com.simprints.infra.authstore.AuthStore
import com.simprints.feature.dashboard.settings.troubleshooting.overview.usecase.CollectNetworkInformationUseCase
import com.simprints.feature.dashboard.settings.troubleshooting.overview.usecase.PingServerUseCase
import com.simprints.feature.dashboard.settings.troubleshooting.overview.usecase.PingServerUseCase.PingResult
import com.simprints.testtools.common.coroutines.TestCoroutineRule
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.every
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
Expand All @@ -31,6 +33,12 @@ class OverviewViewModelTest {
@MockK
private lateinit var collectLicencesUseCase: CollectLicenceStatesUseCase

@MockK
private lateinit var collectNetworkInformationUseCase: CollectNetworkInformationUseCase

@MockK
private lateinit var pingServerUseCase: PingServerUseCase

private lateinit var viewModel: OverviewViewModel

@Before
Expand All @@ -40,20 +48,45 @@ class OverviewViewModelTest {
viewModel = OverviewViewModel(
collectIds = collectIdsUseCase,
collectLicenseStates = collectLicencesUseCase,
collectNetworkInformation = collectNetworkInformationUseCase,
doServerPing = pingServerUseCase,
)
}

@Test
fun `sets when data collected`() = runTest {
every { collectIdsUseCase() } returns "ids"
coEvery { collectLicencesUseCase() } returns "licences"
every { collectNetworkInformationUseCase() } returns "network"

val idsText = viewModel.projectIds.test()
val licenceText = viewModel.licenseStates.test()
val networkText = viewModel.networkStates.test()
val pingResult = viewModel.pingResult.test()

viewModel.collectData()

assertThat(idsText.value()).isNotEmpty()
assertThat(licenceText.value()).isNotEmpty()
assertThat(networkText.value()).isNotEmpty()
assertThat(pingResult.value()).isInstanceOf(PingResult.NotDone::class.java)
}

@Test
fun `propagates server ping result`() = runTest {
val pingResult = viewModel.pingResult.test()

every { pingServerUseCase.invoke() } returns flowOf(
PingResult.InProgress,
PingResult.Success("message"),
)

viewModel.pingServer()

assertThat(pingResult.valueHistory()).containsExactly(
PingResult.NotDone,
PingResult.InProgress,
PingResult.Success("message"),
)
}
}
Loading