From f19cb1bc339125effae81e5a3d3532f95e27e0a4 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Mon, 18 May 2026 20:38:35 +0200 Subject: [PATCH 1/2] feat: add debug CRL refresh controls --- .../android/di/accountScoped/DebugModule.kt | 16 ++++++++ .../wire/android/ui/debug/DebugDataOptions.kt | 8 ++++ .../android/ui/debug/DebugDataOptionsState.kt | 1 + .../ui/debug/DebugDataOptionsViewModel.kt | 23 +++++++++++ .../android/ui/debug/DebugToolsOptions.kt | 41 +++++++++++++++++++ .../wire/android/ui/home/AppSyncViewModel.kt | 19 ++++++++- .../android/ui/home/AppSyncViewModelTest.kt | 8 ++++ .../debug/DebugDataOptionsViewModelTest.kt | 37 ++++++++++++++++- kalium | 2 +- 9 files changed, 152 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt index c60703ddad7..494e8fcfe5a 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/DebugModule.kt @@ -27,7 +27,9 @@ import com.wire.kalium.logic.feature.debug.GetDebugE2EICertificateExpirationUseC import com.wire.kalium.logic.feature.debug.GetFeatureConfigUseCase import com.wire.kalium.logic.feature.debug.GetConversationCryptoStatsUseCase import com.wire.kalium.logic.feature.debug.GetConversationEpochFromCCUseCase +import com.wire.kalium.logic.feature.debug.ObserveDebugCRLExpirationAfterOneMinuteUseCase import com.wire.kalium.logic.feature.debug.RepairFaultyRemovalKeysUseCase +import com.wire.kalium.logic.feature.debug.SetDebugCRLExpirationAfterOneMinuteUseCase import com.wire.kalium.logic.feature.debug.SetDebugE2EICertificateExpirationUseCase import dagger.Module import dagger.Provides @@ -94,6 +96,20 @@ class DebugModule { fun provideSetDebugE2EICertificateExpirationUseCase(debugScope: DebugScope): SetDebugE2EICertificateExpirationUseCase = debugScope.setDebugE2EICertificateExpiration + @ViewModelScoped + @Provides + fun provideObserveDebugCRLExpirationAfterOneMinuteUseCase( + debugScope: DebugScope + ): ObserveDebugCRLExpirationAfterOneMinuteUseCase = + debugScope.observeDebugCRLExpirationAfterOneMinute + + @ViewModelScoped + @Provides + fun provideSetDebugCRLExpirationAfterOneMinuteUseCase( + debugScope: DebugScope + ): SetDebugCRLExpirationAfterOneMinuteUseCase = + debugScope.setDebugCRLExpirationAfterOneMinute + @ViewModelScoped @Provides fun provideGetConversationEpochFromCCUseCase(debugScope: DebugScope): GetConversationEpochFromCCUseCase = diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt index ddbe202fc30..76c07c9c5dd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt @@ -86,6 +86,8 @@ fun DebugDataOptions( handleE2EIEnrollmentResult = viewModel::handleE2EIEnrollmentResult, dismissCertificateDialog = viewModel::dismissCertificateDialog, checkCrlRevocationList = viewModel::checkCrlRevocationList, + forceCRLExpirationAfterOneMinute = viewModel.state.forceCRLExpirationAfterOneMinute, + onForceCRLExpirationAfterOneMinuteChange = viewModel::forceCRLExpirationAfterOneMinute, onResendFCMToken = viewModel::forceSendFCMToken, onEnableAsyncNotificationsChange = viewModel::enableAsyncNotifications, onShowFeatureFlags = onShowFeatureFlags, @@ -110,6 +112,8 @@ fun DebugDataOptionsContent( handleE2EIEnrollmentResult: (FinalizeEnrollmentResult) -> Unit, dismissCertificateDialog: () -> Unit, checkCrlRevocationList: () -> Unit, + forceCRLExpirationAfterOneMinute: Boolean, + onForceCRLExpirationAfterOneMinuteChange: (Boolean) -> Unit, onResendFCMToken: () -> Unit, onShowFeatureFlags: () -> Unit, onShowCryptoStats: () -> Unit, @@ -248,6 +252,8 @@ fun DebugDataOptionsContent( onRestartSlowSyncForRecovery = onRestartSlowSyncForRecovery, onForceUpdateApiVersions = onForceUpdateApiVersions, checkCrlRevocationList = checkCrlRevocationList, + forceCRLExpirationAfterOneMinute = forceCRLExpirationAfterOneMinute, + onForceCRLExpirationAfterOneMinuteChange = onForceCRLExpirationAfterOneMinuteChange, onResendFCMToken = onResendFCMToken, isAsyncNotificationsEnabled = state.isAsyncNotificationsEnabled, onEnableAsyncNotificationsChange = onEnableAsyncNotificationsChange @@ -408,6 +414,8 @@ fun PreviewOtherDebugOptions() = WireTheme { handleE2EIEnrollmentResult = {}, dismissCertificateDialog = {}, checkCrlRevocationList = {}, + forceCRLExpirationAfterOneMinute = false, + onForceCRLExpirationAfterOneMinuteChange = {}, onResendFCMToken = {}, onEnableAsyncNotificationsChange = {}, onShowFeatureFlags = {}, diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt index c32d10f8792..6bfef97f267 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsState.kt @@ -26,6 +26,7 @@ data class DebugDataOptionsState( val showCertificate: Boolean = false, val startGettingE2EICertificate: Boolean = false, val e2eiCertificateExpirationSeconds: Long = 360L, + val forceCRLExpirationAfterOneMinute: Boolean = false, val analyticsTrackingId: String = "null", val isFederationEnabled: Boolean = false, val currentApiVersion: String = "null", diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt index 6d7842c07c2..b6614d94918 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptionsViewModel.kt @@ -45,6 +45,8 @@ import com.wire.kalium.logic.feature.debug.StartUsingAsyncNotificationsResult import com.wire.kalium.logic.feature.debug.StartUsingAsyncNotificationsUseCase import com.wire.kalium.logic.feature.debug.GetDebugE2EICertificateExpirationUseCase import com.wire.kalium.logic.feature.debug.MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS +import com.wire.kalium.logic.feature.debug.ObserveDebugCRLExpirationAfterOneMinuteUseCase +import com.wire.kalium.logic.feature.debug.SetDebugCRLExpirationAfterOneMinuteUseCase import com.wire.kalium.logic.feature.debug.SetDebugE2EICertificateExpirationUseCase import com.wire.kalium.logic.feature.debug.TargetedRepairParam import com.wire.kalium.logic.feature.e2ei.CheckCrlRevocationListUseCase @@ -79,6 +81,7 @@ interface DebugDataOptionsViewModel { val e2eiCertificateExpirationInputState: TextFieldState get() = TextFieldState("6") fun currentAccount(): UserId = UserId("value", "domain") fun checkCrlRevocationList() {} + fun forceCRLExpirationAfterOneMinute(enabled: Boolean) {} fun restartSlowSyncForRecovery() {} fun enrollE2EICertificate() {} fun updateE2EICertificateExpiration(seconds: Long) {} @@ -113,6 +116,8 @@ class DebugDataOptionsViewModelImpl private val repairFaultyRemovalKeys: RepairFaultyRemovalKeysUseCase, private val getDebugE2EICertificateExpiration: GetDebugE2EICertificateExpirationUseCase, private val setDebugE2EICertificateExpiration: SetDebugE2EICertificateExpirationUseCase, + private val observeDebugCRLExpirationAfterOneMinute: ObserveDebugCRLExpirationAfterOneMinuteUseCase, + private val setDebugCRLExpirationAfterOneMinute: SetDebugCRLExpirationAfterOneMinuteUseCase, ) : ViewModel(), DebugDataOptionsViewModel { private companion object { val DEFAULT_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS = 90.days.inWholeSeconds @@ -133,6 +138,7 @@ class DebugDataOptionsViewModelImpl init { observeAsyncNotificationsEnabledData() observeMlsMetadata() + observeDebugCRLExpiration() observeE2EICertificateExpirationInput() setGitHashAndDeviceId() setAnalyticsTrackingId() @@ -205,6 +211,15 @@ class DebugDataOptionsViewModelImpl } } + override fun forceCRLExpirationAfterOneMinute(enabled: Boolean) { + viewModelScope.launch { + setDebugCRLExpirationAfterOneMinute(enabled) + if (enabled) { + checkCrlRevocationList(forceUpdate = true) + } + } + } + override fun restartSlowSyncForRecovery() { viewModelScope.launch { restartSlowSyncProcessForRecovery() @@ -373,6 +388,14 @@ class DebugDataOptionsViewModelImpl } } + private fun observeDebugCRLExpiration() { + viewModelScope.launch { + observeDebugCRLExpirationAfterOneMinute().collect { enabled -> + state = state.copy(forceCRLExpirationAfterOneMinute = enabled) + } + } + } + private fun loadDebugE2EICertificateExpiration() { viewModelScope.launch { val currentExpiration = getDebugE2EICertificateExpiration() diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugToolsOptions.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugToolsOptions.kt index 548716134ea..37bd44ef037 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugToolsOptions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugToolsOptions.kt @@ -49,6 +49,8 @@ private fun DebugToolsOptionsPreview() { onRestartSlowSyncForRecovery = {}, onForceUpdateApiVersions = {}, checkCrlRevocationList = {}, + forceCRLExpirationAfterOneMinute = false, + onForceCRLExpirationAfterOneMinuteChange = {}, onResendFCMToken = {}, isAsyncNotificationsEnabled = true, onEnableAsyncNotificationsChange = {} @@ -63,6 +65,8 @@ internal fun DebugToolsOptions( onRestartSlowSyncForRecovery: () -> Unit, onForceUpdateApiVersions: () -> Unit, checkCrlRevocationList: () -> Unit, + forceCRLExpirationAfterOneMinute: Boolean, + onForceCRLExpirationAfterOneMinuteChange: (Boolean) -> Unit, onResendFCMToken: () -> Unit, isAsyncNotificationsEnabled: Boolean, onEnableAsyncNotificationsChange: (Boolean) -> Unit, @@ -76,6 +80,8 @@ internal fun DebugToolsOptions( onRestartSlowSyncForRecovery = onRestartSlowSyncForRecovery, onForceUpdateApiVersions = onForceUpdateApiVersions, checkCrlRevocationList = checkCrlRevocationList, + forceCRLExpirationAfterOneMinute = forceCRLExpirationAfterOneMinute, + onForceCRLExpirationAfterOneMinuteChange = onForceCRLExpirationAfterOneMinuteChange, isAsyncNotificationsEnabled = isAsyncNotificationsEnabled, onEnableAsyncNotificationsChange = onEnableAsyncNotificationsChange ) @@ -91,6 +97,8 @@ private fun PrivateBuildDebugToolsOptions( onRestartSlowSyncForRecovery: () -> Unit, onForceUpdateApiVersions: () -> Unit, checkCrlRevocationList: () -> Unit, + forceCRLExpirationAfterOneMinute: Boolean, + onForceCRLExpirationAfterOneMinuteChange: (Boolean) -> Unit, isAsyncNotificationsEnabled: Boolean, onEnableAsyncNotificationsChange: (Boolean) -> Unit, ) { @@ -100,6 +108,10 @@ private fun PrivateBuildDebugToolsOptions( onCheckedChange = onDisableEventProcessingChange ) RestartSlowSyncButton(onClick = onRestartSlowSyncForRecovery) + ForceCRLExpirationAfterOneMinuteSwitch( + isEnabled = forceCRLExpirationAfterOneMinute, + onCheckedChange = onForceCRLExpirationAfterOneMinuteChange + ) CheckCrlRevocationButton(onClick = checkCrlRevocationList) ForceUpdateApiVersionsButton(onClick = onForceUpdateApiVersions) EnableAsyncNotifications(isAsyncNotificationsEnabled, onEnableAsyncNotificationsChange) @@ -142,6 +154,35 @@ private fun DisableEventProcessingSwitch( ) } +@Composable +private fun ForceCRLExpirationAfterOneMinuteSwitch( + isEnabled: Boolean = false, + onCheckedChange: ((Boolean) -> Unit)?, +) { + RowItemTemplate( + title = { + Text( + style = MaterialTheme.wireTypography.body01, + color = MaterialTheme.wireColorScheme.onBackground, + text = "Force CRL expiry after 1 minute", + modifier = Modifier.padding(start = dimensions().spacing8x) + ) + }, + actions = { + WireSwitch( + checked = isEnabled, + onCheckedChange = onCheckedChange, + modifier = Modifier + .padding(end = dimensions().spacing8x) + .size( + width = dimensions().buttonSmallMinSize.width, + height = dimensions().buttonSmallMinSize.height + ) + ) + } + ) +} + @Composable private fun RestartSlowSyncButton( onClick: () -> Unit, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/AppSyncViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/AppSyncViewModel.kt index a9bc143870a..5679236a4d2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/AppSyncViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/AppSyncViewModel.kt @@ -21,9 +21,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.appLogger import com.wire.android.util.dispatchers.DispatcherProvider +import com.wire.kalium.logic.feature.debug.ObserveDebugCRLExpirationAfterOneMinuteUseCase import com.wire.kalium.logic.sync.ForegroundActionsUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.Instant @@ -34,14 +37,27 @@ import kotlin.time.Duration.Companion.minutes @HiltViewModel class AppSyncViewModel @Inject constructor( private val foregroundActionsUseCase: ForegroundActionsUseCase, + observeDebugCRLExpirationAfterOneMinute: ObserveDebugCRLExpirationAfterOneMinuteUseCase, private val dispatcher: DispatcherProvider, ) : ViewModel() { - private val minIntervalBetweenPulls: Duration = MIN_INTERVAL_BETWEEN_PULLS + private var minIntervalBetweenPulls: Duration = MIN_INTERVAL_BETWEEN_PULLS private var lastPullInstant: Instant? = null private var syncDataJob: Job? = null + init { + observeDebugCRLExpirationAfterOneMinute() + .onEach { isForced -> + minIntervalBetweenPulls = if (isForced) { + MIN_INTERVAL_BETWEEN_FORCED_CRL_PULLS + } else { + MIN_INTERVAL_BETWEEN_PULLS + } + } + .launchIn(viewModelScope) + } + fun startSyncingAppConfig() { if (isSyncing()) return @@ -75,5 +91,6 @@ class AppSyncViewModel @Inject constructor( companion object { val MIN_INTERVAL_BETWEEN_PULLS = 60.minutes + val MIN_INTERVAL_BETWEEN_FORCED_CRL_PULLS = 1.minutes } } diff --git a/app/src/test/kotlin/com/wire/android/ui/home/AppSyncViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/AppSyncViewModelTest.kt index 4c9261c193d..b2a3ce4a482 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/AppSyncViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/AppSyncViewModelTest.kt @@ -19,12 +19,15 @@ package com.wire.android.ui.home import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.TestDispatcherProvider +import com.wire.kalium.logic.feature.debug.ObserveDebugCRLExpirationAfterOneMinuteUseCase import com.wire.kalium.logic.sync.ForegroundActionsUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test @@ -65,8 +68,12 @@ class AppSyncViewModelTest { @MockK lateinit var foregroundActionsUseCase: ForegroundActionsUseCase + @MockK + lateinit var observeDebugCRLExpirationAfterOneMinute: ObserveDebugCRLExpirationAfterOneMinuteUseCase + init { MockKAnnotations.init(this) + every { observeDebugCRLExpirationAfterOneMinute() } returns flowOf(false) } fun withForegroundActionsUseCase(delayMs: Long = 0) { @@ -78,6 +85,7 @@ class AppSyncViewModelTest { fun arrange(testDispatcher: TestDispatcherProvider, block: Arrangement.() -> Unit) = apply(block).let { this to AppSyncViewModel( foregroundActionsUseCase = foregroundActionsUseCase, + observeDebugCRLExpirationAfterOneMinute = observeDebugCRLExpirationAfterOneMinute, dispatcher = testDispatcher ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/settings/debug/DebugDataOptionsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/settings/debug/DebugDataOptionsViewModelTest.kt index 53c6be6c51c..8b05d9c2aa3 100644 --- a/app/src/test/kotlin/com/wire/android/ui/settings/debug/DebugDataOptionsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/settings/debug/DebugDataOptionsViewModelTest.kt @@ -42,6 +42,8 @@ import com.wire.kalium.logic.feature.debug.StartUsingAsyncNotificationsResult import com.wire.kalium.logic.feature.debug.StartUsingAsyncNotificationsUseCase import com.wire.kalium.logic.feature.debug.GetDebugE2EICertificateExpirationUseCase import com.wire.kalium.logic.feature.debug.MIN_DEBUG_E2EI_CERTIFICATE_EXPIRATION_SECONDS +import com.wire.kalium.logic.feature.debug.ObserveDebugCRLExpirationAfterOneMinuteUseCase +import com.wire.kalium.logic.feature.debug.SetDebugCRLExpirationAfterOneMinuteUseCase import com.wire.kalium.logic.feature.debug.SetDebugE2EICertificateExpirationUseCase import com.wire.kalium.logic.feature.e2ei.CheckCrlRevocationListUseCase import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountResult @@ -287,6 +289,25 @@ class DebugDataOptionsViewModelTest { coVerify(exactly = 1) { arrangement.setDebugE2EICertificateExpiration(420) } assertEquals(420, viewModel.state.e2eiCertificateExpirationSeconds) } + + @Test + fun `given CRL force expiration is observed, then view state contains loaded value`() = runTest { + val (_, viewModel) = DebugDataOptionsHiltArrangement() + .withDebugCRLExpirationAfterOneMinute(true) + .arrange() + + assertEquals(true, viewModel.state.forceCRLExpirationAfterOneMinute) + } + + @Test + fun `when forcing CRL expiration after one minute, then setting use case is called`() = runTest { + val (arrangement, viewModel) = DebugDataOptionsHiltArrangement().arrange() + + viewModel.forceCRLExpirationAfterOneMinute(true) + + coVerify(exactly = 1) { arrangement.setDebugCRLExpirationAfterOneMinute(true) } + coVerify(exactly = 1) { arrangement.checkCrlRevocationList(forceUpdate = true) } + } } internal class DebugDataOptionsHiltArrangement { @@ -335,6 +356,12 @@ internal class DebugDataOptionsHiltArrangement { @MockK lateinit var setDebugE2EICertificateExpiration: SetDebugE2EICertificateExpirationUseCase + @MockK + lateinit var observeDebugCRLExpirationAfterOneMinute: ObserveDebugCRLExpirationAfterOneMinuteUseCase + + @MockK + lateinit var setDebugCRLExpirationAfterOneMinute: SetDebugCRLExpirationAfterOneMinuteUseCase + private val viewModel by lazy { DebugDataOptionsViewModelImpl( context = context, @@ -352,7 +379,9 @@ internal class DebugDataOptionsHiltArrangement { observeAsyncNotificationsEnabled = observeIsConsumableNotificationsEnabled, repairFaultyRemovalKeys = repairFaultyRemovalKeysUseCase, getDebugE2EICertificateExpiration = getDebugE2EICertificateExpiration, - setDebugE2EICertificateExpiration = setDebugE2EICertificateExpiration + setDebugE2EICertificateExpiration = setDebugE2EICertificateExpiration, + observeDebugCRLExpirationAfterOneMinute = observeDebugCRLExpirationAfterOneMinute, + setDebugCRLExpirationAfterOneMinute = setDebugCRLExpirationAfterOneMinute ) } @@ -393,6 +422,8 @@ internal class DebugDataOptionsHiltArrangement { withObserveIsConsumableNotificationsEnabled(false) coEvery { getDebugE2EICertificateExpiration() } returns 360 coEvery { setDebugE2EICertificateExpiration(any()) } returns Unit + every { observeDebugCRLExpirationAfterOneMinute() } returns flowOf(false) + coEvery { setDebugCRLExpirationAfterOneMinute(any()) } returns Unit } } @@ -400,6 +431,10 @@ internal class DebugDataOptionsHiltArrangement { coEvery { getDebugE2EICertificateExpiration() } returns expiration } + fun withDebugCRLExpirationAfterOneMinute(enabled: Boolean) = apply { + every { observeDebugCRLExpirationAfterOneMinute() } returns flowOf(enabled) + } + suspend fun withObserveIsConsumableNotificationsEnabled(isEnabled: Boolean = false) = apply { coEvery { observeIsConsumableNotificationsEnabled() diff --git a/kalium b/kalium index 9a0b40b5dc7..967e08b7c01 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 9a0b40b5dc7cbf73dc156fc690222a18d4673759 +Subproject commit 967e08b7c012cacdc55e65b9cfd1f64d07b541b6 From 9a50f3c08b3775299fc0d2af840dba34da22f21e Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Thu, 21 May 2026 12:12:43 +0200 Subject: [PATCH 2/2] kalium update --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 967e08b7c01..d62cb852afb 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 967e08b7c012cacdc55e65b9cfd1f64d07b541b6 +Subproject commit d62cb852afb2815902e1d61041d884c561e5f286