1
0
Fork 0
mirror of https://github.com/bitwarden/android.git synced 2025-03-03 04:55:54 +03:00
This commit is contained in:
Dave Severns 2024-12-13 18:09:20 -05:00
parent 3fd1e86a03
commit 8cbda5e5a0
8 changed files with 145 additions and 5 deletions
app/src
main/java/com/x8bit/bitwarden
MainActivity.ktMainViewModel.kt
data/platform
datasource/network/util
manager/di
repository/model
ui
auth/feature/login
platform/composition
standard/java/com/x8bit/bitwarden/data/platform/manager

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.KeyEvent
@ -11,6 +12,7 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -25,6 +27,8 @@ import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavScreen
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManagerImpl
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@ -76,6 +80,8 @@ class MainActivity : AppCompatActivity() {
setContent {
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
val navController = rememberNavController()
val activity = LocalContext.current as Activity
val intentManager: IntentManager = IntentManagerImpl(activity)
EventsEffect(viewModel = mainViewModel) { event ->
when (event) {
is MainEvent.CompleteAccessibilityAutofill -> {
@ -94,10 +100,15 @@ class MainActivity : AppCompatActivity() {
)
.show()
}
is MainEvent.ShareText -> intentManager.shareText(event.logText)
}
}
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
LocalManagerProvider {
LocalManagerProvider(
activity = activity,
intentManager = intentManager,
) {
BitwardenTheme(theme = state.theme) {
RootNavScreen(
onSplashScreenRemoved = { shouldShowSplashScreen = false },

View file

@ -24,6 +24,7 @@ import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.InMemoryLogManager
import com.x8bit.bitwarden.data.platform.util.isAddTotpLoginItemFromAuthenticator
import com.x8bit.bitwarden.data.vault.manager.model.VaultStateEvent
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
@ -72,6 +73,7 @@ class MainViewModel @Inject constructor(
private val environmentRepository: EnvironmentRepository,
private val savedStateHandle: SavedStateHandle,
private val clock: Clock,
inMemoryLogManager: InMemoryLogManager,
) : BaseViewModel<MainState, MainEvent, MainAction>(
initialState = MainState(
theme = settingsRepository.appTheme,
@ -161,6 +163,10 @@ class MainViewModel @Inject constructor(
settingsRepository.storeUserHasLoggedInValue(it.userId)
}
}
inMemoryLogManager.publishedLogsFlow.onEach {
sendAction(MainAction.Internal.ShareLogText(it))
}.launchIn(viewModelScope)
}
override fun handleAction(action: MainAction) {
@ -180,9 +186,14 @@ class MainViewModel @Inject constructor(
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
is MainAction.Internal.ShareLogText -> handleShareLogText(action)
}
}
private fun handleShareLogText(action: MainAction.Internal.ShareLogText) {
sendEvent(MainEvent.ShareText(action.logText))
}
private fun handleOpenDebugMenu() {
sendEvent(MainEvent.NavigateToDebugMenu)
}
@ -485,6 +496,8 @@ sealed class MainAction {
* Indicates a relevant change in the current vault lock state.
*/
data object VaultUnlockStateChange : Internal()
data class ShareLogText(val logText: String) : Internal()
}
}
@ -518,4 +531,7 @@ sealed class MainEvent {
* Show a toast with the given [message].
*/
data class ShowToast(val message: Text) : MainEvent()
data class ShareText(val logText: String) : MainEvent() {
}
}

View file

@ -4,6 +4,7 @@ import okio.ByteString.Companion.decodeBase64
import java.net.UnknownHostException
import java.nio.charset.Charset
import java.security.cert.CertPathValidatorException
import java.security.cert.CertificateException
import java.util.Base64
import javax.net.ssl.SSLHandshakeException
@ -49,6 +50,19 @@ fun Throwable?.isNoConnectionError(): Boolean {
*/
fun Throwable?.isSslHandShakeError(): Boolean {
return this is SSLHandshakeException ||
this is CertificateException ||
this is CertPathValidatorException ||
this?.cause?.isSslHandShakeError() ?: false
}
fun Throwable?.getCertPathValidatorExceptionOrNull(): CertPathValidatorException? {
return this?.cause?.getCertPathValidatorExceptionOrNull()
?: when (this) {
is CertPathValidatorException -> this
else -> null
}
}
fun CertPathValidatorException?.getCertificateChainListOrEmpty(): List<String> {
return this?.certPath?.certificates?.map { it.toString() } ?: emptyList()
}

View file

@ -60,6 +60,8 @@ import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.InMemoryLogManager
import com.x8bit.bitwarden.data.platform.repository.model.InMemoryLogManagerImpl
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import dagger.Module
@ -247,9 +249,11 @@ object PlatformManagerModule {
fun provideLogsManager(
legacyAppCenterMigrator: LegacyAppCenterMigrator,
settingsRepository: SettingsRepository,
inMemoryLogManager: InMemoryLogManager,
): LogsManager = LogsManagerImpl(
settingsRepository = settingsRepository,
legacyAppCenterMigrator = legacyAppCenterMigrator,
inMemoryLogManager = inMemoryLogManager,
)
@Provides
@ -310,4 +314,8 @@ object PlatformManagerModule {
authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
)
@Provides
@Singleton
fun provideInMemoryLogManager(): InMemoryLogManager = InMemoryLogManagerImpl()
}

View file

@ -0,0 +1,70 @@
package com.x8bit.bitwarden.data.platform.repository.model
import com.x8bit.bitwarden.data.platform.datasource.network.util.getCertPathValidatorExceptionOrNull
import com.x8bit.bitwarden.data.platform.datasource.network.util.getCertificateChainListOrEmpty
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow
data class LoggableResult(
val message: String?,
val throwable: Throwable?,
) {
val hasNothingToCapture: Boolean = message.isNullOrEmpty() && throwable == null
}
interface InMemoryLogManager {
val publishedLogsFlow: Flow<String>
fun clearLogs()
fun registerLoggableResult(loggableResult: LoggableResult)
fun publishLogs()
}
class InMemoryLogManagerImpl: InMemoryLogManager {
private val loggableResults = MemoryLimitedBuffer<LoggableResult>(maxSize = 1)
private val publishedLogsChannel: Channel<String> = Channel(Channel.BUFFERED)
private val publishedLogsSendChannel: SendChannel<String> = publishedLogsChannel
override val publishedLogsFlow: Flow<String> = publishedLogsChannel.consumeAsFlow()
override fun clearLogs() {
loggableResults.clear()
}
override fun registerLoggableResult(loggableResult: LoggableResult) {
if (loggableResult.hasNothingToCapture) return
loggableResults.add(loggableResult)
}
override fun publishLogs() {
if (loggableResults.getAll().isEmpty()) return
val publishableLogs = loggableResults.getAll().joinToString(
separator = "\n"
) { result ->
val message = "Message: ${result.message}\n".takeIf { result.message != null }.orEmpty()
val stacktraceMessage = "Stacktrace: ${result.throwable?.stackTraceToString()}\n"
.takeIf { result.throwable != null }.orEmpty()
message + stacktraceMessage + result.throwable.getCertPathValidatorExceptionOrNull()
.getCertificateChainListOrEmpty()
}
clearLogs()
publishedLogsSendChannel.trySend(publishableLogs)
}
}
class MemoryLimitedBuffer<T>(private val maxSize: Int) {
private val buffer = ArrayDeque<T>(maxSize)
fun add(item: T) {
if (buffer.size >= maxSize) {
buffer.removeFirst() // Remove oldest item if buffer is full
}
buffer.addLast(item) // Add new item to the end
}
fun getAll(): List<T> = buffer.toList()
fun clear() {
buffer.clear()
}
}

View file

@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.InMemoryLogManager
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
@ -39,6 +40,7 @@ class LoginViewModel @Inject constructor(
environmentRepository: EnvironmentRepository,
private val authRepository: AuthRepository,
private val vaultRepository: VaultRepository,
private val inMemoryLogManager: InMemoryLogManager,
) : BaseViewModel<LoginState, LoginEvent, LoginAction>(
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE]
@ -207,6 +209,7 @@ class LoginViewModel @Inject constructor(
private fun handleErrorDialogDismiss() {
mutableStateFlow.update { it.copy(dialogState = null) }
inMemoryLogManager.publishLogs()
}
private fun handleCaptchaTokenReceived(tokenResult: CaptchaCallbackTokenResult) {

View file

@ -31,19 +31,19 @@ import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManagerImp
*/
@Composable
fun LocalManagerProvider(
activity: Activity = LocalContext.current as Activity,
intentManager: IntentManager = IntentManagerImpl(activity),
content: @Composable () -> Unit,
) {
val activity = LocalContext.current as Activity
val fido2IntentManager: IntentManager = IntentManagerImpl(activity)
val fido2CompletionManager =
if (isBuildVersionBelow(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
Fido2CompletionManagerUnsupportedApiImpl
} else {
Fido2CompletionManagerImpl(activity, fido2IntentManager)
Fido2CompletionManagerImpl(activity, intentManager)
}
CompositionLocalProvider(
LocalPermissionsManager provides PermissionsManagerImpl(activity),
LocalIntentManager provides fido2IntentManager,
LocalIntentManager provides intentManager,
LocalExitManager provides ExitManagerImpl(activity),
LocalBiometricsManager provides BiometricsManagerImpl(activity),
LocalNfcManager provides NfcManagerImpl(activity),

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.manager
import android.util.Log
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import com.x8bit.bitwarden.BuildConfig
@ -7,6 +8,8 @@ import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.model.InMemoryLogManager
import com.x8bit.bitwarden.data.platform.repository.model.LoggableResult
import timber.log.Timber
/**
@ -16,6 +19,7 @@ import timber.log.Timber
class LogsManagerImpl(
private val settingsRepository: SettingsRepository,
legacyAppCenterMigrator: LegacyAppCenterMigrator,
private val inMemoryLogManager: InMemoryLogManager,
) : LogsManager {
private val nonfatalErrorTree: NonfatalErrorTree = NonfatalErrorTree()
@ -52,6 +56,7 @@ class LogsManagerImpl(
init {
legacyAppCenterMigrator.migrateIfNecessary()
isEnabled = settingsRepository.isCrashLoggingEnabled
Timber.plant(LocalLoggerTree())
}
private inner class NonfatalErrorTree : Timber.Tree() {
@ -59,6 +64,19 @@ class LogsManagerImpl(
t?.let { trackNonFatalException(BitwardenNonfatalException(message, it)) }
}
}
private inner class LocalLoggerTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (priority == Log.WARN || priority == Log.ERROR) {
inMemoryLogManager.registerLoggableResult(
LoggableResult(
message = message,
throwable = t,
)
)
}
}
}
}
private class BitwardenNonfatalException(