Skip to content

Commit

Permalink
Fix Black Screen After Login (#3459)
Browse files Browse the repository at this point in the history
* Refactor AppMainActivity's onCreate

* Refactor onCreate

* Switch to dispatcherProvider

* Run P2P init in the background

- Run PIN hashing in the background

* Revert update

* Add missing import

* Remove synclistener registration to onResume

* Refactor hashing algorithm

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Switch coroutine for password hashing

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Resolve black screen bug on main activity launch

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Fix SecureSharedPreferenceTest

* Fix failing tests

* Run spotlessApply

* Update failing tests

---------

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>
Co-authored-by: Elly Kitoto <junkmailstoelly@gmail.com>
Co-authored-by: Benjamin Mwalimu <dubdabasoduba@gmail.com>
  • Loading branch information
3 people committed Sep 3, 2024
1 parent c1b8b34 commit 077ba54
Show file tree
Hide file tree
Showing 15 changed files with 285 additions and 188 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import java.util.Base64
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.jetbrains.annotations.VisibleForTesting
import org.smartregister.fhircore.engine.auth.AuthCredentials
import org.smartregister.fhircore.engine.util.extension.decodeJson
Expand Down Expand Up @@ -71,15 +74,21 @@ class SecureSharedPreference @Inject constructor(@ApplicationContext val context
.getString(SharedPreferenceKey.LOGIN_CREDENTIAL_KEY.name, null)
?.decodeJson<AuthCredentials>()

fun saveSessionPin(pin: CharArray) {
suspend fun saveSessionPin(pin: CharArray, onSavedPin: () -> Unit) {
val randomSaltBytes = get256RandomBytes()
secureSharedPreferences.edit {
putString(
SharedPreferenceKey.LOGIN_PIN_SALT.name,
Base64.getEncoder().encodeToString(randomSaltBytes),
)
putString(SharedPreferenceKey.LOGIN_PIN_KEY.name, pin.toPasswordHash(randomSaltBytes))
putString(
SharedPreferenceKey.LOGIN_PIN_KEY.name,
coroutineScope {
async(Dispatchers.Default) { pin.toPasswordHash(randomSaltBytes) }.await()
},
)
}
onSavedPin()
}

@VisibleForTesting fun get256RandomBytes() = 256.getRandomBytesOfSize()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package org.smartregister.fhircore.engine.util

import android.os.Build
import java.nio.charset.StandardCharsets
import java.security.SecureRandom
import java.util.Arrays
Expand All @@ -28,22 +27,14 @@ fun CharArray.toPasswordHash(salt: ByteArray) = passwordHashString(this, salt)

@VisibleForTesting
fun passwordHashString(password: CharArray, salt: ByteArray): String {
val pbKeySpec = PBEKeySpec(password, salt, 800000, 256)
val secretKeyFactory =
SecretKeyFactory.getInstance(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
"PBKDF2withHmacSHA256"
} else {
"PBKDF2WithHmacSHA1"
},
)
val pbKeySpec = PBEKeySpec(password, salt, 180000, 512)
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA512")
return secretKeyFactory.generateSecret(pbKeySpec).encoded.toString(StandardCharsets.UTF_8)
}

fun Int.getRandomBytesOfSize(): ByteArray {
val random = SecureRandom()
val randomSaltBytes = ByteArray(this)
random.nextBytes(randomSaltBytes)
SecureRandom().nextBytes(randomSaltBytes)
return randomSaltBytes
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import androidx.test.core.app.ApplicationProvider
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
Expand Down Expand Up @@ -73,9 +76,13 @@ internal class SecureSharedPreferenceTest : RobolectricTest() {
}

@Test
fun testSaveAndRetrievePin() {
fun testSaveAndRetrievePin() = runBlocking {
every { secureSharedPreference.get256RandomBytes() } returns byteArrayOf(-100, 0, 100, 101)
secureSharedPreference.saveSessionPin(pin = "1234".toCharArray())

val onSavedPinMock = mockk<() -> Unit>(relaxed = true)
secureSharedPreference.saveSessionPin(pin = "1234".toCharArray(), onSavedPin = onSavedPinMock)

verify { onSavedPinMock.invoke() }
Assert.assertEquals(
"1234".toCharArray().toPasswordHash(byteArrayOf(-100, 0, 100, 101)),
secureSharedPreference.retrieveSessionPin(),
Expand All @@ -85,10 +92,13 @@ internal class SecureSharedPreferenceTest : RobolectricTest() {
}

@Test
fun testResetSharedPrefsClearsData() {
fun testResetSharedPrefsClearsData() = runBlocking {
every { secureSharedPreference.get256RandomBytes() } returns byteArrayOf(-128, 100, 112, 127)

secureSharedPreference.saveSessionPin(pin = "6699".toCharArray())
val onSavedPinMock = mockk<() -> Unit>(relaxed = true)
secureSharedPreference.saveSessionPin(pin = "6699".toCharArray(), onSavedPin = onSavedPinMock)

verify { onSavedPinMock.invoke() }

val retrievedSessionPin = secureSharedPreference.retrieveSessionPin()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class PinLoginScreenKtTest {
composeRule.setContent {
PinLoginPage(
onSetPin = {},
showProgressBar = false,
showError = false,
onMenuLoginClicked = {},
forgotPin = {},
Expand Down Expand Up @@ -71,6 +72,7 @@ class PinLoginScreenKtTest {
composeRule.setContent {
PinLoginPage(
onSetPin = {},
showProgressBar = false,
showError = false,
onMenuLoginClicked = {},
forgotPin = {},
Expand Down Expand Up @@ -107,6 +109,7 @@ class PinLoginScreenKtTest {
composeRule.setContent {
PinLoginPage(
onSetPin = {},
showProgressBar = false,
showError = true,
onMenuLoginClicked = {},
forgotPin = {},
Expand All @@ -117,7 +120,6 @@ class PinLoginScreenKtTest {
setupPin = false,
pinLength = 4,
showLogo = true,
showProgressBar = true,
),
onShowPinError = {},
onPinEntered = { _: CharArray, _: (Boolean) -> Unit -> },
Expand All @@ -131,6 +133,7 @@ class PinLoginScreenKtTest {
composeRule.setContent {
PinLoginPage(
onSetPin = {},
showProgressBar = true,
showError = false,
onMenuLoginClicked = {},
forgotPin = {},
Expand All @@ -141,7 +144,6 @@ class PinLoginScreenKtTest {
setupPin = true,
pinLength = 4,
showLogo = false,
showProgressBar = true,
),
onShowPinError = {},
onPinEntered = { _: CharArray, _: (Boolean) -> Unit -> },
Expand All @@ -159,6 +161,7 @@ class PinLoginScreenKtTest {
composeRule.setContent {
PinLoginPage(
onSetPin = {},
showProgressBar = false,
showError = false,
onMenuLoginClicked = {},
forgotPin = {},
Expand All @@ -169,7 +172,6 @@ class PinLoginScreenKtTest {
setupPin = false,
pinLength = 1,
showLogo = false,
showProgressBar = true,
),
onShowPinError = {},
onPinEntered = { _: CharArray, _: (Boolean) -> Unit -> },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ class AppSettingActivity : AppCompatActivity() {
loadConfigurations(appSettingActivity)
}
} else if (!BuildConfig.OPENSRP_APP_ID.isNullOrEmpty()) {
// this part simulates what the user would have done manually via the text field and button
appSettingViewModel.onApplicationIdChanged(BuildConfig.OPENSRP_APP_ID)
appSettingViewModel.fetchConfigurations(appSettingActivity)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import android.view.View
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.material.ExperimentalMaterialApi
import androidx.core.os.bundleOf
import androidx.compose.runtime.getValue
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
Expand All @@ -39,23 +40,19 @@ import dagger.hilt.android.AndroidEntryPoint
import io.sentry.android.navigation.SentryNavigationListener
import java.time.Instant
import javax.inject.Inject
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.hl7.fhir.r4.model.IdType
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig
import org.smartregister.fhircore.engine.configuration.app.LocationLogOptions
import org.smartregister.fhircore.engine.configuration.app.SyncStrategy
import org.smartregister.fhircore.engine.configuration.workflow.ActionTrigger
import org.smartregister.fhircore.engine.datastore.ProtoDataStore
import org.smartregister.fhircore.engine.datastore.syncLocationIdsProtoStore
import org.smartregister.fhircore.engine.domain.model.LauncherType
import org.smartregister.fhircore.engine.rulesengine.services.LocationCoordinate
import org.smartregister.fhircore.engine.sync.OnSyncListener
import org.smartregister.fhircore.engine.sync.SyncListenerManager
import org.smartregister.fhircore.engine.ui.base.BaseMultiLanguageActivity
import org.smartregister.fhircore.engine.util.extension.isDeviceOnline
import org.smartregister.fhircore.engine.util.DispatcherProvider
import org.smartregister.fhircore.engine.util.extension.parcelable
import org.smartregister.fhircore.engine.util.extension.serializable
import org.smartregister.fhircore.engine.util.extension.showToast
Expand All @@ -64,7 +61,6 @@ import org.smartregister.fhircore.engine.util.location.PermissionUtils
import org.smartregister.fhircore.quest.R
import org.smartregister.fhircore.quest.event.AppEvent
import org.smartregister.fhircore.quest.event.EventBus
import org.smartregister.fhircore.quest.navigation.NavigationArg
import org.smartregister.fhircore.quest.ui.questionnaire.QuestionnaireActivity
import org.smartregister.fhircore.quest.ui.shared.QuestionnaireHandler
import org.smartregister.fhircore.quest.ui.shared.models.QuestionnaireSubmission
Expand All @@ -79,6 +75,9 @@ open class AppMainActivity : BaseMultiLanguageActivity(), QuestionnaireHandler,
@Inject lateinit var protoDataStore: ProtoDataStore

@Inject lateinit var eventBus: EventBus

@Inject lateinit var dispatcherProvider: DispatcherProvider

val appMainViewModel by viewModels<AppMainViewModel>()
private val sentryNavListener =
SentryNavigationListener(enableNavigationBreadcrumbs = true, enableNavigationTracing = true)
Expand All @@ -104,70 +103,34 @@ open class AppMainActivity : BaseMultiLanguageActivity(), QuestionnaireHandler,
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupLocationServices()
setContentView(R.layout.activity_main)
lifecycleScope.launch(dispatcherProvider.main()) {
val navController =
(supportFragmentManager.findFragmentById(R.id.nav_host) as NavHostFragment).navController

val startDestinationConfig =
appMainViewModel.applicationConfiguration.navigationStartDestination
val startDestinationArgs =
when (startDestinationConfig.launcherType) {
LauncherType.REGISTER -> {
val topMenuConfig = appMainViewModel.navigationConfiguration.clientRegisters.first()
val clickAction = topMenuConfig.actions?.find { it.trigger == ActionTrigger.ON_CLICK }
bundleOf(
NavigationArg.SCREEN_TITLE to
if (startDestinationConfig.screenTitle.isNullOrEmpty()) {
topMenuConfig.display
} else startDestinationConfig.screenTitle,
NavigationArg.REGISTER_ID to
if (startDestinationConfig.id.isNullOrEmpty()) {
clickAction?.id ?: topMenuConfig.id
} else startDestinationConfig.id,
)
val graph =
withContext(dispatcherProvider.io()) {
navController.navInflater.inflate(R.navigation.application_nav_graph).apply {
val startDestination =
when (
appMainViewModel.applicationConfiguration.navigationStartDestination.launcherType
) {
LauncherType.MAP -> R.id.geoWidgetLauncherFragment
LauncherType.REGISTER -> R.id.registerFragment
}
setStartDestination(startDestination)
}
}
LauncherType.MAP -> bundleOf(NavigationArg.GEO_WIDGET_ID to startDestinationConfig.id)
}

// Retrieve the navController directly from the NavHostFragment
val navController =
(supportFragmentManager.findFragmentById(R.id.nav_host) as NavHostFragment).navController

val graph =
navController.navInflater.inflate(R.navigation.application_nav_graph).apply {
val startDestination =
when (appMainViewModel.applicationConfiguration.navigationStartDestination.launcherType) {
LauncherType.MAP -> R.id.geoWidgetLauncherFragment
LauncherType.REGISTER -> R.id.registerFragment
}
setStartDestination(startDestination)
appMainViewModel.run {
navController.setGraph(graph, getStartDestinationArgs())
retrieveAppMainUiState()
withContext(dispatcherProvider.io()) { schedulePeriodicJobs(this@AppMainActivity) }
}
setupLocationServices()

navController.setGraph(graph, startDestinationArgs)

// Setup the drawer and schedule jobs
appMainViewModel.run {
retrieveAppMainUiState()
if (isDeviceOnline()) {
// Do not schedule sync until location selected when strategy is RelatedEntityLocation
// Use applicationConfiguration.usePractitionerAssignedLocationOnSync to identify
// if we need to trigger sync based on assigned locations or not
if (applicationConfiguration.syncStrategy.contains(SyncStrategy.RelatedEntityLocation)) {
if (
applicationConfiguration.usePractitionerAssignedLocationOnSync ||
runBlocking { syncLocationIdsProtoStore.data.firstOrNull() }?.isNotEmpty() == true
) {
schedulePeriodicSync()
}
} else {
schedulePeriodicSync()
}
} else {
showToast(
getString(org.smartregister.fhircore.engine.R.string.sync_failed),
Toast.LENGTH_LONG,
)
}
schedulePeriodicJobs()
findViewById<View>(R.id.mainScreenProgressBar).apply { visibility = View.GONE }
findViewById<View>(R.id.mainScreenProgressBarText).apply { visibility = View.GONE }
}
}

Expand Down
Loading

0 comments on commit 077ba54

Please sign in to comment.