Restoration connected to sdk

This commit is contained in:
Milan Cerovsky 2025-04-16 11:43:34 +02:00
parent a07d7fcc8e
commit aa72d1f92f
15 changed files with 226 additions and 78 deletions

View File

@ -211,7 +211,7 @@ ZIP_321_VERSION = 0.0.6
# WARNING: Ensure a non-snapshot version is used before releasing to production # WARNING: Ensure a non-snapshot version is used before releasing to production
ZCASH_BIP39_VERSION=1.0.9 ZCASH_BIP39_VERSION=1.0.9
# WARNING: Ensure a non-snapshot version is used before releasing to production # WARNING: Ensure a non-snapshot version is used before releasing to production
ZCASH_SDK_VERSION=2.2.11 ZCASH_SDK_VERSION=2.2.11-SNAPSHOT
# Toolchain is the Java version used to build the application, which is separate from the # Toolchain is the Java version used to build the application, which is separate from the
# Java version used to run the application. # Java version used to run the application.

View File

@ -13,6 +13,7 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -39,40 +40,34 @@ import java.text.DateFormatSymbols
import java.time.Month import java.time.Month
import java.time.Year import java.time.Year
import java.time.YearMonth import java.time.YearMonth
import kotlin.math.absoluteValue
import kotlin.math.pow import kotlin.math.pow
@Suppress("MagicNumber") @Suppress("MagicNumber")
@Composable @Composable
fun ZashiYearMonthWheelDatePicker( fun ZashiYearMonthWheelDatePicker(
selection: YearMonth,
onSelectionChange: (YearMonth) -> Unit, onSelectionChange: (YearMonth) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
verticallyVisibleItems: Int = 3, verticallyVisibleItems: Int = 3,
startYear: Year = Year.of(2016), startInclusive: YearMonth = YearMonth.of(2018, 10),
endYear: Year = Year.now(), endInclusive: YearMonth = YearMonth.now(),
selectionYear: YearMonth = YearMonth.now(),
) { ) {
val latestOnSelectionChanged by rememberUpdatedState(onSelectionChange) val latestOnSelectionChanged by rememberUpdatedState(onSelectionChange)
var selectedDate by remember { mutableStateOf(selectionYear) }
val months =
listOf(
Month.JANUARY,
Month.FEBRUARY,
Month.MARCH,
Month.APRIL,
Month.MAY,
Month.JUNE,
Month.JULY,
Month.AUGUST,
Month.SEPTEMBER,
Month.OCTOBER,
Month.NOVEMBER,
Month.DECEMBER
)
val years = (startYear.value..endYear.value).toList()
LaunchedEffect(selectedDate) { var state by remember {
Twig.debug { "Selection changed: $selectedDate" } mutableStateOf(
latestOnSelectionChanged(selectedDate) InternalState(
selectedDate = selection,
months = getMonthsForYear(Year.of(selection.year), startInclusive, endInclusive),
years = (startInclusive.year..endInclusive.year).map { Year.of(it) }.toList()
)
)
}
LaunchedEffect(state.selectedDate) {
Twig.debug { "Selection changed: ${state.selectedDate}" }
latestOnSelectionChanged(state.selectedDate)
} }
Box(modifier = modifier) { Box(modifier = modifier) {
@ -92,14 +87,18 @@ fun ZashiYearMonthWheelDatePicker(
Spacer(Modifier.weight(.5f)) Spacer(Modifier.weight(.5f))
WheelLazyList( WheelLazyList(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
selection = maxOf(months.indexOf(selectedDate.month), 0), selection = state.selectedMonthIndex,
itemCount = months.size, itemCount = state.months.size,
itemVerticalOffset = verticallyVisibleItems, itemVerticalOffset = verticallyVisibleItems,
isInfiniteScroll = true, isInfiniteScroll = false,
onFocusItem = { selectedDate = selectedDate.withMonth(months[it].value) }, onFocusItem = {
state = state.copy(
selectedDate = state.selectedDate.withMonth(state.months[it].value)
)
},
itemContent = { itemContent = {
Text( Text(
text = DateFormatSymbols().months[months[it].value - 1], text = DateFormatSymbols().months[state.months[it].value - 1],
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillParentMaxWidth(), modifier = Modifier.fillParentMaxWidth(),
style = ZashiTypography.header6, style = ZashiTypography.header6,
@ -110,14 +109,28 @@ fun ZashiYearMonthWheelDatePicker(
) )
WheelLazyList( WheelLazyList(
modifier = Modifier.weight(.75f), modifier = Modifier.weight(.75f),
selection = years.indexOf(selectedDate.year), selection = state.selectedYearIndex,
itemCount = years.size, itemCount = state.years.size,
itemVerticalOffset = verticallyVisibleItems, itemVerticalOffset = verticallyVisibleItems,
isInfiniteScroll = false, isInfiniteScroll = false,
onFocusItem = { selectedDate = selectedDate.withYear(years[it]) }, onFocusItem = {
val year = state.years[it]
val normalizedSelectedMonth = getSelectedMonthForYear(
year = year,
selectedMonth = state.selectedDate.month,
startYearMonth = startInclusive,
endYearMonth = endInclusive
)
val months = getMonthsForYear(year, startInclusive, endInclusive)
val selectedDate = state.selectedDate.withYear(year.value).withMonth(normalizedSelectedMonth.value)
state = state.copy(
selectedDate = selectedDate,
months = months
)
},
itemContent = { itemContent = {
Text( Text(
text = years[it].toString(), text = state.years[it].toString(),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillParentMaxWidth(), modifier = Modifier.fillParentMaxWidth(),
style = ZashiTypography.header6, style = ZashiTypography.header6,
@ -131,6 +144,79 @@ fun ZashiYearMonthWheelDatePicker(
} }
} }
private fun getMonthsForYear(year: Year, startYearMonth: YearMonth, endYearMonth: YearMonth): List<Month> {
return when (year.value) {
startYearMonth.year -> {
(startYearMonth.month.value..Month.DECEMBER.value).map { index ->
Month.entries.first { it.value == index }
}
}
endYearMonth.year -> {
(Month.JANUARY.value..endYearMonth.month.value).map { index ->
Month.entries.first { it.value == index }
}
}
else -> {
listOf(
Month.JANUARY,
Month.FEBRUARY,
Month.MARCH,
Month.APRIL,
Month.MAY,
Month.JUNE,
Month.JULY,
Month.AUGUST,
Month.SEPTEMBER,
Month.OCTOBER,
Month.NOVEMBER,
Month.DECEMBER
)
}
}
}
private fun getSelectedMonthForYear(
year: Year,
selectedMonth: Month,
startYearMonth: YearMonth,
endYearMonth: YearMonth
): Month {
return when (year.value) {
startYearMonth.year -> {
val months = (startYearMonth.month.value..Month.DECEMBER.value).map { index ->
Month.entries.first { it.value == index }
}
if (selectedMonth in months) selectedMonth else months.findClosest(selectedMonth)
}
endYearMonth.year -> {
val months = (Month.JANUARY.value..endYearMonth.month.value).map { index ->
Month.entries.first { it.value == index }
}
if (selectedMonth in months) selectedMonth else months.findClosest(selectedMonth)
}
else -> selectedMonth
}
}
private fun List<Month>.findClosest(target: Month): Month {
var closestNumber = this[0] // Initialize with the first element
var minDifference = (this[0].value - target.value).absoluteValue
for (number in this) {
val difference = (number.value - target.value).absoluteValue
if (difference < minDifference) {
minDifference = difference
closestNumber = number
}
}
return closestNumber
}
@Suppress("MagicNumber", "ContentSlotReused") @Suppress("MagicNumber", "ContentSlotReused")
@Composable @Composable
private fun WheelLazyList( private fun WheelLazyList(
@ -155,10 +241,8 @@ private fun WheelLazyList(
val isScrollInProgress = state.isScrollInProgress val isScrollInProgress = state.isScrollInProgress
LaunchedEffect(itemCount) { LaunchedEffect(itemCount) {
coroutineScope.launch {
state.scrollToItem(startIndex) state.scrollToItem(startIndex)
} }
}
LaunchedEffect(key1 = isScrollInProgress) { LaunchedEffect(key1 = isScrollInProgress) {
if (!isScrollInProgress) { if (!isScrollInProgress) {
@ -286,3 +370,13 @@ private fun calculateIndexToFocus(
} }
return index return index
} }
@Immutable
private data class InternalState(
val selectedDate: YearMonth,
val months: List<Month>,
val years: List<Year>
) {
val selectedYearIndex = years.map { it.value }.indexOf(selectedDate.year)
val selectedMonthIndex = maxOf(months.indexOf(selectedDate.month), 0)
}

View File

@ -134,10 +134,10 @@ fun MainActivity.OnboardingNavigation() {
AndroidRestoreBDHeight(it.toRoute()) AndroidRestoreBDHeight(it.toRoute())
} }
composable<RestoreBDDate> { composable<RestoreBDDate> {
AndroidRestoreBDDate() AndroidRestoreBDDate(it.toRoute())
} }
composable<RestoreBDEstimation> { composable<RestoreBDEstimation> {
AndroidRestoreBDEstimation() AndroidRestoreBDEstimation(it.toRoute())
} }
dialog<SeedInfo>( dialog<SeedInfo>(
dialogProperties = dialogProperties =

View File

@ -6,14 +6,15 @@ import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
@Composable @Composable
fun AndroidRestoreBDDate() { fun AndroidRestoreBDDate(args: RestoreBDDate) {
val vm = koinViewModel<RestoreBDDateViewModel>() val vm = koinViewModel<RestoreBDDateViewModel> { parametersOf(args) }
val state by vm.state.collectAsStateWithLifecycle() val state by vm.state.collectAsStateWithLifecycle()
RestoreBDDateView(state) BackHandler(enabled = state != null) { state?.onBack?.invoke() }
BackHandler { state.onBack() } state?.let { RestoreBDDateView(it) }
} }
@Serializable @Serializable
data object RestoreBDDate data class RestoreBDDate(val seed: String)

View File

@ -2,9 +2,12 @@ package co.electriccoin.zcash.ui.screen.restore.date
import co.electriccoin.zcash.ui.design.component.ButtonState import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.IconButtonState import co.electriccoin.zcash.ui.design.component.IconButtonState
import java.time.YearMonth
data class RestoreBDDateState( data class RestoreBDDateState(
val selection: YearMonth,
val next: ButtonState, val next: ButtonState,
val dialogButton: IconButtonState, val dialogButton: IconButtonState,
val onBack: () -> Unit val onBack: () -> Unit,
val onYearMonthChange: (YearMonth) -> Unit,
) )

View File

@ -42,6 +42,7 @@ import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.orDark import co.electriccoin.zcash.ui.design.util.orDark
import co.electriccoin.zcash.ui.design.util.scaffoldPadding import co.electriccoin.zcash.ui.design.util.scaffoldPadding
import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.design.util.stringRes
import java.time.YearMonth
@Composable @Composable
fun RestoreBDDateView(state: RestoreBDDateState) { fun RestoreBDDateView(state: RestoreBDDateState) {
@ -84,7 +85,8 @@ private fun Content(
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
ZashiYearMonthWheelDatePicker( ZashiYearMonthWheelDatePicker(
onSelectionChange = {}, selection = state.selection,
onSelectionChange = state.onYearMonthChange,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
@ -150,8 +152,10 @@ private fun Preview() =
state = state =
RestoreBDDateState( RestoreBDDateState(
next = ButtonState(stringRes("Estimate")) {}, next = ButtonState(stringRes("Estimate")) {},
dialogButton = IconButtonState(R.drawable.ic_restore_dialog) {}, dialogButton = IconButtonState(R.drawable.ic_help) {},
onBack = {} onBack = {},
onYearMonthChange = {},
selection = YearMonth.now()
) )
) )
} }

View File

@ -1,6 +1,12 @@
package co.electriccoin.zcash.ui.screen.restore.date package co.electriccoin.zcash.ui.screen.restore.date
import android.content.Context
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.zcash.ui.NavigationRouter import co.electriccoin.zcash.ui.NavigationRouter
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.ButtonState import co.electriccoin.zcash.ui.design.component.ButtonState
@ -9,27 +15,64 @@ import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.restore.estimation.RestoreBDEstimation import co.electriccoin.zcash.ui.screen.restore.estimation.RestoreBDEstimation
import co.electriccoin.zcash.ui.screen.restore.info.SeedInfo import co.electriccoin.zcash.ui.screen.restore.info.SeedInfo
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlinx.datetime.toKotlinInstant
import java.time.YearMonth
import java.time.ZoneId
class RestoreBDDateViewModel( class RestoreBDDateViewModel(
private val navigationRouter: NavigationRouter private val args: RestoreBDDate,
private val navigationRouter: NavigationRouter,
private val context: Context,
) : ViewModel() { ) : ViewModel() {
val state: StateFlow<RestoreBDDateState> = MutableStateFlow(createState()).asStateFlow()
private fun createState() = private val selection = MutableStateFlow<YearMonth>(YearMonth.now())
val state: StateFlow<RestoreBDDateState?> = selection
.map {
createState(it)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = null
)
private fun createState(selection: YearMonth) =
RestoreBDDateState( RestoreBDDateState(
next = ButtonState(stringRes(R.string.restore_bd_height_btn), onClick = ::onEstimateClick), next = ButtonState(stringRes(R.string.restore_bd_height_btn), onClick = ::onEstimateClick),
dialogButton = dialogButton =
IconButtonState( IconButtonState(
icon = co.electriccoin.zcash.ui.design.R.drawable.ic_info, icon = R.drawable.ic_help,
onClick = ::onInfoButtonClick, onClick = ::onInfoButtonClick,
), ),
onBack = ::onBack, onBack = ::onBack,
onYearMonthChange = ::onYearMonthChange,
selection = selection
) )
private fun onEstimateClick() { private fun onEstimateClick() {
navigationRouter.forward(RestoreBDEstimation) viewModelScope.launch {
val instant = selection.value.atDay(1)
.atStartOfDay()
.atZone(ZoneId.systemDefault())
.toInstant()
.toKotlinInstant()
val bday = SdkSynchronizer.estimateBirthdayHeight(
context = context,
date = instant,
network = ZcashNetwork.fromResources(context)
)
navigationRouter.forward(RestoreBDEstimation(seed = args.seed, blockHeight = bday.value))
}
} }
private fun onBack() { private fun onBack() {
@ -39,4 +82,8 @@ class RestoreBDDateViewModel(
private fun onInfoButtonClick() { private fun onInfoButtonClick() {
navigationRouter.forward(SeedInfo) navigationRouter.forward(SeedInfo)
} }
private fun onYearMonthChange(yearMonth: YearMonth) {
selection.update { yearMonth }
}
} }

View File

@ -6,14 +6,18 @@ import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
@Composable @Composable
fun AndroidRestoreBDEstimation() { fun AndroidRestoreBDEstimation(args: RestoreBDEstimation) {
val vm = koinViewModel<RestoreBDEstimationViewModel>() val vm = koinViewModel<RestoreBDEstimationViewModel> { parametersOf(args) }
val state by vm.state.collectAsStateWithLifecycle() val state by vm.state.collectAsStateWithLifecycle()
RestoreBDEstimationView(state) RestoreBDEstimationView(state)
BackHandler { state.onBack() } BackHandler { state.onBack() }
} }
@Serializable @Serializable
data object RestoreBDEstimation data class RestoreBDEstimation(
val seed: String,
val blockHeight: Long
)

View File

@ -133,7 +133,7 @@ private fun Preview() =
state = state =
RestoreBDEstimationState( RestoreBDEstimationState(
restore = ButtonState(stringRes("Estimate")) {}, restore = ButtonState(stringRes("Estimate")) {},
dialogButton = IconButtonState(R.drawable.ic_restore_dialog) {}, dialogButton = IconButtonState(R.drawable.ic_help) {},
onBack = {}, onBack = {},
text = stringRes("123456"), text = stringRes("123456"),
copy = ButtonState(stringRes("Copy"), icon = R.drawable.ic_copy) {} copy = ButtonState(stringRes("Copy"), icon = R.drawable.ic_copy) {}

View File

@ -1,8 +1,11 @@
package co.electriccoin.zcash.ui.screen.restore.estimation package co.electriccoin.zcash.ui.screen.restore.estimation
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.SeedPhrase
import co.electriccoin.zcash.ui.NavigationRouter import co.electriccoin.zcash.ui.NavigationRouter
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.usecase.RestoreWalletUseCase
import co.electriccoin.zcash.ui.design.component.ButtonState import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.IconButtonState import co.electriccoin.zcash.ui.design.component.IconButtonState
import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.design.util.stringRes
@ -12,7 +15,9 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
class RestoreBDEstimationViewModel( class RestoreBDEstimationViewModel(
private val navigationRouter: NavigationRouter private val args: RestoreBDEstimation,
private val navigationRouter: NavigationRouter,
private val restoreWallet: RestoreWalletUseCase
) : ViewModel() { ) : ViewModel() {
val state: StateFlow<RestoreBDEstimationState> = MutableStateFlow(createState()).asStateFlow() val state: StateFlow<RestoreBDEstimationState> = MutableStateFlow(createState()).asStateFlow()
@ -20,17 +25,20 @@ class RestoreBDEstimationViewModel(
RestoreBDEstimationState( RestoreBDEstimationState(
dialogButton = dialogButton =
IconButtonState( IconButtonState(
icon = co.electriccoin.zcash.ui.design.R.drawable.ic_info, icon = R.drawable.ic_help,
onClick = ::onInfoButtonClick, onClick = ::onInfoButtonClick,
), ),
onBack = ::onBack, onBack = ::onBack,
text = stringRes("123456"), text = stringRes(args.blockHeight.toString()),
copy = ButtonState(stringRes(R.string.restore_bd_estimation_copy), icon = R.drawable.ic_copy) {}, copy = ButtonState(stringRes(R.string.restore_bd_estimation_copy), icon = R.drawable.ic_copy) {},
restore = ButtonState(stringRes(R.string.restore_bd_estimation_restore), onClick = ::onRestoreClick), restore = ButtonState(stringRes(R.string.restore_bd_estimation_restore), onClick = ::onRestoreClick),
) )
private fun onRestoreClick() { private fun onRestoreClick() {
// do nothing restoreWallet(
seedPhrase = SeedPhrase.new(args.seed),
birthday = BlockHeight.new(args.blockHeight)
)
} }
private fun onBack() { private fun onBack() {

View File

@ -205,7 +205,7 @@ private fun Preview() =
state = state =
RestoreBDHeightState( RestoreBDHeightState(
onBack = {}, onBack = {},
dialogButton = IconButtonState(R.drawable.ic_restore_dialog) {}, dialogButton = IconButtonState(R.drawable.ic_help) {},
blockHeight = TextFieldState(stringRes("")) {}, blockHeight = TextFieldState(stringRes("")) {},
estimate = ButtonState(stringRes("Estimate")) {}, estimate = ButtonState(stringRes("Estimate")) {},
restore = ButtonState(stringRes("Restore")) {} restore = ButtonState(stringRes("Restore")) {}

View File

@ -54,7 +54,7 @@ class RestoreBDHeightViewModel(
onBack = ::onBack, onBack = ::onBack,
dialogButton = dialogButton =
IconButtonState( IconButtonState(
icon = co.electriccoin.zcash.ui.design.R.drawable.ic_info, icon = R.drawable.ic_help,
onClick = ::onInfoButtonClick, onClick = ::onInfoButtonClick,
), ),
restore = restore =
@ -74,7 +74,7 @@ class RestoreBDHeightViewModel(
} }
private fun onEstimateClick() { private fun onEstimateClick() {
navigationRouter.forward(RestoreBDDate) navigationRouter.forward(RestoreBDDate(seed = restoreBDHeight.seed))
} }
private fun onRestoreClick() { private fun onRestoreClick() {

View File

@ -297,7 +297,7 @@ private fun Preview() =
} }
), ),
onBack = {}, onBack = {},
dialogButton = IconButtonState(R.drawable.ic_restore_dialog) {}, dialogButton = IconButtonState(R.drawable.ic_help) {},
nextButton = nextButton =
ButtonState( ButtonState(
text = stringRes("Next"), text = stringRes("Next"),

View File

@ -130,7 +130,7 @@ class RestoreSeedViewModel(
onBack = ::onBack, onBack = ::onBack,
dialogButton = dialogButton =
IconButtonState( IconButtonState(
icon = co.electriccoin.zcash.ui.design.R.drawable.ic_info, icon = R.drawable.ic_help,
onClick = ::onInfoButtonClick onClick = ::onInfoButtonClick
), ),
nextButton = nextButton =

View File

@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M9.09,9C9.325,8.332 9.789,7.768 10.4,7.409C11.011,7.05 11.729,6.919 12.427,7.039C13.125,7.158 13.759,7.522 14.215,8.064C14.671,8.606 14.921,9.292 14.92,10C14.92,12 11.92,13 11.92,13M12,17H12.01M22,12C22,17.523 17.523,22 12,22C6.477,22 2,17.523 2,12C2,6.477 6.477,2 12,2C17.523,2 22,6.477 22,12Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>