Add notifications permission

This commit is contained in:
Rafał Kobyłko 2023-02-17 19:08:25 +01:00
parent 7927edf99b
commit 43ad8d9114
18 changed files with 633 additions and 92 deletions

View File

@ -9,6 +9,8 @@
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" /> <uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission <uses-permission
android:name="com.google.android.gms.permission.AD_ID" android:name="com.google.android.gms.permission.AD_ID"
tools:node="remove" /> tools:node="remove" />

View File

@ -11,6 +11,7 @@ import com.twofasapp.environment.AppConfig
import com.twofasapp.externalimport.domain.GoogleAuthenticatorImporter import com.twofasapp.externalimport.domain.GoogleAuthenticatorImporter
import com.twofasapp.permissions.CameraPermissionRequest import com.twofasapp.permissions.CameraPermissionRequest
import com.twofasapp.permissions.CameraPermissionRequestFlow import com.twofasapp.permissions.CameraPermissionRequestFlow
import com.twofasapp.permissions.NotificationsPermissionRequestFlow
import com.twofasapp.services.analytics.AnalyticsServiceFirebase import com.twofasapp.services.analytics.AnalyticsServiceFirebase
import com.twofasapp.services.googledrive.GoogleDriveService import com.twofasapp.services.googledrive.GoogleDriveService
import com.twofasapp.services.googledrive.GoogleDriveServiceImpl import com.twofasapp.services.googledrive.GoogleDriveServiceImpl
@ -43,6 +44,7 @@ val applicationModule = module {
factory { CameraPermissionRequest(activityContext()) } factory { CameraPermissionRequest(activityContext()) }
factory { CameraPermissionRequestFlow(activityContext()) } factory { CameraPermissionRequestFlow(activityContext()) }
factory { NotificationsPermissionRequestFlow(activityContext()) }
single<Dispatchers> { AppDispatchers() } single<Dispatchers> { AppDispatchers() }

View File

@ -6,6 +6,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import com.twofasapp.browserextension.ui.browser.BrowserDetailsScreenFactory import com.twofasapp.browserextension.ui.browser.BrowserDetailsScreenFactory
import com.twofasapp.browserextension.ui.main.BrowserExtensionScreenFactory import com.twofasapp.browserextension.ui.main.BrowserExtensionScreenFactory
import com.twofasapp.browserextension.ui.main.permission.BrowserExtensionPermissionScreenFactory
import com.twofasapp.browserextension.ui.pairing.progress.PairingProgressScreenFactory import com.twofasapp.browserextension.ui.pairing.progress.PairingProgressScreenFactory
import com.twofasapp.browserextension.ui.pairing.scan.PairingScanScreenFactory import com.twofasapp.browserextension.ui.pairing.scan.PairingScanScreenFactory
import com.twofasapp.settings.ui.main.SettingsMainScreenFactory import com.twofasapp.settings.ui.main.SettingsMainScreenFactory
@ -19,6 +20,7 @@ class SettingsRouterImpl(
private val pairingProgressScreenFactory: PairingProgressScreenFactory, private val pairingProgressScreenFactory: PairingProgressScreenFactory,
private val pairingScanScreenFactory: PairingScanScreenFactory, private val pairingScanScreenFactory: PairingScanScreenFactory,
private val browserDetailsScreenFactory: BrowserDetailsScreenFactory, private val browserDetailsScreenFactory: BrowserDetailsScreenFactory,
private val browserExtensionPermissionScreenFactory: BrowserExtensionPermissionScreenFactory,
) : SettingsRouter() { ) : SettingsRouter() {
companion object { companion object {
@ -29,6 +31,7 @@ class SettingsRouterImpl(
private const val BROWSER_EXTENSION = "browser_extension" private const val BROWSER_EXTENSION = "browser_extension"
private const val BROWSER_DETAILS = "browser_details/{$ARG_EXTENSION_ID}" private const val BROWSER_DETAILS = "browser_details/{$ARG_EXTENSION_ID}"
private const val PAIRING_SCAN = "pairing_scan" private const val PAIRING_SCAN = "pairing_scan"
private const val PERMISSION = "permission"
private const val PAIRING_PROGRESS = "pairing_progress/{$ARG_EXTENSION_ID}" private const val PAIRING_PROGRESS = "pairing_progress/{$ARG_EXTENSION_ID}"
} }
@ -40,6 +43,7 @@ class SettingsRouterImpl(
browserDetailsScreenFactory.create(it.arguments?.getString(ARG_EXTENSION_ID).orEmpty()) browserDetailsScreenFactory.create(it.arguments?.getString(ARG_EXTENSION_ID).orEmpty())
}) })
builder.composable(route = PAIRING_SCAN, content = { pairingScanScreenFactory.create() }) builder.composable(route = PAIRING_SCAN, content = { pairingScanScreenFactory.create() })
builder.composable(route = PERMISSION, content = { browserExtensionPermissionScreenFactory.create() })
builder.composable(route = PAIRING_PROGRESS, content = { builder.composable(route = PAIRING_PROGRESS, content = {
pairingProgressScreenFactory.create(it.arguments?.getString(ARG_EXTENSION_ID).orEmpty()) pairingProgressScreenFactory.create(it.arguments?.getString(ARG_EXTENSION_ID).orEmpty())
}) })
@ -59,6 +63,7 @@ class SettingsRouterImpl(
SettingsDirections.Theme -> navController.navigate(THEME) SettingsDirections.Theme -> navController.navigate(THEME)
SettingsDirections.BrowserExtension -> navController.navigate(BROWSER_EXTENSION) SettingsDirections.BrowserExtension -> navController.navigate(BROWSER_EXTENSION)
SettingsDirections.PairingScan -> navController.navigate(PAIRING_SCAN) { popUpTo(BROWSER_EXTENSION) } SettingsDirections.PairingScan -> navController.navigate(PAIRING_SCAN) { popUpTo(BROWSER_EXTENSION) }
SettingsDirections.Permission -> navController.navigate(PERMISSION) { popUpTo(BROWSER_EXTENSION) }
is SettingsDirections.PairingProgress -> navController.navigate( is SettingsDirections.PairingProgress -> navController.navigate(
PAIRING_PROGRESS.replace("{$ARG_EXTENSION_ID}", direction.extensionId) PAIRING_PROGRESS.replace("{$ARG_EXTENSION_ID}", direction.extensionId)

View File

@ -34,6 +34,7 @@ dependencies {
implementation(libs.bundles.fastAdapter) implementation(libs.bundles.fastAdapter)
implementation(libs.bundles.rxJava) implementation(libs.bundles.rxJava)
implementation(libs.bundles.appCompat) implementation(libs.bundles.appCompat)
implementation(libs.bundles.accompanist)
implementation(libs.lottie) implementation(libs.lottie)
implementation(libs.timber) implementation(libs.timber)
implementation(libs.kotlinCoroutines) implementation(libs.kotlinCoroutines)

View File

@ -4,7 +4,20 @@ import com.twofasapp.browserextension.data.BrowserExtensionLocalData
import com.twofasapp.browserextension.data.BrowserExtensionLocalDataImpl import com.twofasapp.browserextension.data.BrowserExtensionLocalDataImpl
import com.twofasapp.browserextension.data.BrowserExtensionRemoteData import com.twofasapp.browserextension.data.BrowserExtensionRemoteData
import com.twofasapp.browserextension.data.BrowserExtensionRemoteDataImpl import com.twofasapp.browserextension.data.BrowserExtensionRemoteDataImpl
import com.twofasapp.browserextension.domain.* import com.twofasapp.browserextension.domain.ApproveLoginRequestCase
import com.twofasapp.browserextension.domain.DeletePairedBrowserCase
import com.twofasapp.browserextension.domain.DenyLoginRequestCase
import com.twofasapp.browserextension.domain.EncryptCodeCase
import com.twofasapp.browserextension.domain.FetchPairedBrowsersCase
import com.twofasapp.browserextension.domain.FetchTokenRequestsCase
import com.twofasapp.browserextension.domain.FetchTokenRequestsCaseImpl
import com.twofasapp.browserextension.domain.ObserveMobileDeviceCase
import com.twofasapp.browserextension.domain.ObserveMobileDeviceCaseImpl
import com.twofasapp.browserextension.domain.ObservePairedBrowsersCase
import com.twofasapp.browserextension.domain.ObservePairedBrowsersCaseImpl
import com.twofasapp.browserextension.domain.PairBrowserCase
import com.twofasapp.browserextension.domain.RegisterMobileDeviceCase
import com.twofasapp.browserextension.domain.UpdateMobileDeviceCase
import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository
import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepositoryImpl import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepositoryImpl
import com.twofasapp.browserextension.notification.ShowBrowserExtensionRequestNotificationCaseImpl import com.twofasapp.browserextension.notification.ShowBrowserExtensionRequestNotificationCaseImpl
@ -12,6 +25,8 @@ import com.twofasapp.browserextension.ui.browser.BrowserDetailsScreenFactory
import com.twofasapp.browserextension.ui.browser.BrowserDetailsViewModel import com.twofasapp.browserextension.ui.browser.BrowserDetailsViewModel
import com.twofasapp.browserextension.ui.main.BrowserExtensionScreenFactory import com.twofasapp.browserextension.ui.main.BrowserExtensionScreenFactory
import com.twofasapp.browserextension.ui.main.BrowserExtensionViewModel import com.twofasapp.browserextension.ui.main.BrowserExtensionViewModel
import com.twofasapp.browserextension.ui.main.permission.BrowserExtensionPermissionScreenFactory
import com.twofasapp.browserextension.ui.main.permission.BrowserExtensionPermissionViewModel
import com.twofasapp.browserextension.ui.pairing.progress.PairingProgressScreenFactory import com.twofasapp.browserextension.ui.pairing.progress.PairingProgressScreenFactory
import com.twofasapp.browserextension.ui.pairing.progress.PairingProgressViewModel import com.twofasapp.browserextension.ui.pairing.progress.PairingProgressViewModel
import com.twofasapp.browserextension.ui.pairing.scan.PairingScanScreenFactory import com.twofasapp.browserextension.ui.pairing.scan.PairingScanScreenFactory
@ -46,6 +61,7 @@ class BrowserExtensionModule : KoinModule {
singleOf(::DeletePairedBrowserCase) singleOf(::DeletePairedBrowserCase)
viewModelOf(::BrowserExtensionViewModel) viewModelOf(::BrowserExtensionViewModel)
viewModelOf(::BrowserExtensionPermissionViewModel)
viewModelOf(::PairingScanViewModel) viewModelOf(::PairingScanViewModel)
viewModelOf(::PairingProgressViewModel) viewModelOf(::PairingProgressViewModel)
viewModelOf(::BrowserExtensionRequestViewModel) viewModelOf(::BrowserExtensionRequestViewModel)
@ -55,5 +71,6 @@ class BrowserExtensionModule : KoinModule {
singleOf(::PairingProgressScreenFactory) singleOf(::PairingProgressScreenFactory)
singleOf(::PairingScanScreenFactory) singleOf(::PairingScanScreenFactory)
singleOf(::BrowserDetailsScreenFactory) singleOf(::BrowserDetailsScreenFactory)
singleOf(::BrowserExtensionPermissionScreenFactory)
} }
} }

View File

@ -1,16 +1,38 @@
package com.twofasapp.browserextension.ui.main package com.twofasapp.browserextension.ui.main
import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button
import androidx.compose.material.* import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -22,25 +44,71 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayout
import com.twofasapp.resources.R import androidx.core.content.ContextCompat
import com.twofasapp.design.compose.* import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.twofasapp.design.compose.ButtonShape
import com.twofasapp.design.compose.ButtonTextColor
import com.twofasapp.design.compose.HeaderEntry
import com.twofasapp.design.compose.SimpleEntry
import com.twofasapp.design.compose.Toolbar
import com.twofasapp.design.compose.dialogs.InputDialog import com.twofasapp.design.compose.dialogs.InputDialog
import com.twofasapp.design.compose.dialogs.RationaleDialog import com.twofasapp.design.compose.dialogs.RationaleDialog
import com.twofasapp.extensions.openBrowserApp import com.twofasapp.extensions.openBrowserApp
import com.twofasapp.navigation.SettingsDirections import com.twofasapp.navigation.SettingsDirections
import com.twofasapp.navigation.SettingsRouter import com.twofasapp.navigation.SettingsRouter
import com.twofasapp.resources.R
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.compose.get import org.koin.androidx.compose.get
@Composable
internal fun RequestPermission(
permission: String,
rationaleTitle: String = "",
rationaleText: String = "",
onGranted: () -> Unit = {},
onDismiss: () -> Unit = {},
) {
var showRationale by remember { mutableStateOf(false) }
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
onGranted()
} else {
showRationale = true
}
}
val permissionCheckResult = ContextCompat.checkSelfPermission(LocalContext.current, permission)
if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) {
onGranted()
} else {
LaunchedEffect(Unit) {
launcher.launch(permission)
}
}
if (showRationale) {
RationaleDialog(
title = rationaleTitle, text = rationaleText, onDismiss = onDismiss
)
}
}
@Composable @Composable
internal fun BrowserExtensionScreen( internal fun BrowserExtensionScreen(
viewModel: BrowserExtensionViewModel = get(), viewModel: BrowserExtensionViewModel = get(),
router: SettingsRouter = get(), router: SettingsRouter = get()
) { ) {
val uiState = viewModel.uiState.collectAsState().value val uiState = viewModel.uiState.collectAsState().value
val scaffoldState = rememberScaffoldState() val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var askForPermission by remember { mutableStateOf(false) }
Scaffold( Scaffold(
scaffoldState = scaffoldState, scaffoldState = scaffoldState,
@ -49,17 +117,28 @@ internal fun BrowserExtensionScreen(
if (uiState.isLoading) return@Scaffold if (uiState.isLoading) return@Scaffold
if (uiState.pairedBrowsers.isEmpty()) { if (uiState.pairedBrowsers.isEmpty()) {
EmptyScreen(viewModel, padding) EmptyScreen(router = router, padding = padding, onAddClick = { askForPermission = true })
} else { } else {
ContentScreen( ContentScreen(
viewModel = viewModel, viewModel = viewModel,
uiState = uiState, uiState = uiState,
router = router, router = router,
padding = padding onAddClick = { askForPermission = true },
padding = padding,
) )
} }
} }
if (askForPermission) {
RequestPermission(
permission = Manifest.permission.CAMERA,
onGranted = { router.navigate(SettingsDirections.PairingScan) },
onDismiss = { askForPermission = false },
rationaleTitle = stringResource(id = R.string.permissions__camera_permission),
rationaleText = stringResource(id = R.string.permissions__camera_permission_description),
)
}
uiState.getMostRecentEvent()?.let { uiState.getMostRecentEvent()?.let {
when (it) { when (it) {
is BrowserExtensionUiState.Event.ShowSnackbarError -> { is BrowserExtensionUiState.Event.ShowSnackbarError -> {
@ -73,32 +152,33 @@ internal fun BrowserExtensionScreen(
} }
} }
@OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
internal fun ContentScreen( internal fun ContentScreen(
viewModel: BrowserExtensionViewModel, viewModel: BrowserExtensionViewModel,
uiState: BrowserExtensionUiState, uiState: BrowserExtensionUiState,
router: SettingsRouter, router: SettingsRouter,
onAddClick: () -> Unit,
padding: PaddingValues, padding: PaddingValues,
) { ) {
val activity = LocalContext.current as Activity
val permissionState = rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
LazyColumn(modifier = Modifier.padding(padding)) { LazyColumn(modifier = Modifier.padding(padding)) {
item { item {
HeaderEntry(text = stringResource(id = R.string.browser__paired_devices_browser_title)) HeaderEntry(text = stringResource(id = R.string.browser__paired_devices_browser_title))
} }
items(uiState.pairedBrowsers, key = { it.id }) { items(uiState.pairedBrowsers, key = { it.id }) {
SimpleEntry( SimpleEntry(title = it.name,
title = it.name,
iconVisibleWhenNotSet = true, iconVisibleWhenNotSet = true,
subtitle = it.formatPairedAt(), subtitle = it.formatPairedAt(),
click = { router.navigate(SettingsDirections.BrowserDetails(extensionId = it.id)) } click = { router.navigate(SettingsDirections.BrowserDetails(extensionId = it.id)) })
)
} }
item { item {
Button( Button(
onClick = { viewModel.onPairBrowserClick() }, onClick = { onAddClick() }, shape = ButtonShape(), modifier = Modifier.padding(start = 72.dp, top = 6.dp, bottom = 2.dp)
shape = ButtonShape(),
modifier = Modifier.padding(start = 72.dp, top = 6.dp, bottom = 2.dp)
) { ) {
Text(text = stringResource(id = R.string.browser__add_new).uppercase(), color = ButtonTextColor()) Text(text = stringResource(id = R.string.browser__add_new).uppercase(), color = ButtonTextColor())
} }
@ -116,14 +196,30 @@ internal fun ContentScreen(
iconEndClick = { viewModel.onEditDeviceClick() }, iconEndClick = { viewModel.onEditDeviceClick() },
) )
} }
}
if (uiState.showRationaleDialog) { if (permissionState.status.isGranted.not()) {
RationaleDialog( item {
title = stringResource(id = R.string.permissions__camera_permission), Divider(Modifier.padding(top = 24.dp, bottom = 24.dp))
text = stringResource(id = R.string.permissions__camera_permission_description), Text(
onDismiss = { viewModel.onRationaleDialogDismiss() } text = stringResource(id = R.string.browser__push_notifications_content),
) style = MaterialTheme.typography.body2.copy(fontSize = 14.sp, color = MaterialTheme.colors.primary),
modifier = Modifier.padding(start = 72.dp, bottom = 8.dp, end = 16.dp)
)
}
item {
Button(
onClick = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", activity.packageName, null))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
activity.startActivity(intent)
},
shape = ButtonShape(),
modifier = Modifier.padding(start = 72.dp, top = 6.dp, bottom = 2.dp)
) {
Text(text = "Enable Notifications".uppercase(), color = ButtonTextColor())
}
}
}
} }
if (uiState.showEditDeviceDialog) { if (uiState.showEditDeviceDialog) {
@ -140,28 +236,27 @@ internal fun ContentScreen(
@Composable @Composable
internal fun EmptyScreen( internal fun EmptyScreen(
viewModel: BrowserExtensionViewModel, router: SettingsRouter,
padding: PaddingValues, padding: PaddingValues,
onAddClick: () -> Unit,
) { ) {
val activity = (LocalContext.current as? Activity) val activity = (LocalContext.current as? Activity)
ConstraintLayout(modifier = Modifier ConstraintLayout(
.fillMaxSize() modifier = Modifier
.padding(padding)) { .fillMaxSize()
.padding(padding)
) {
val (content, pair) = createRefs() val (content, pair) = createRefs()
Column( Column(verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier
verticalArrangement = Arrangement.spacedBy(16.dp), .constrainAs(content) {
horizontalAlignment = Alignment.CenterHorizontally, top.linkTo(parent.top)
modifier = Modifier bottom.linkTo(pair.top)
.constrainAs(content) { start.linkTo(parent.start)
top.linkTo(parent.top) end.linkTo(parent.end)
bottom.linkTo(pair.top) }
start.linkTo(parent.start) .padding(vertical = 16.dp)) {
end.linkTo(parent.end)
}
.padding(vertical = 16.dp)
) {
Image( Image(
painter = painterResource(id = R.drawable.browser_extension_start_image), painter = painterResource(id = R.drawable.browser_extension_start_image),
contentDescription = null, contentDescription = null,
@ -184,28 +279,29 @@ internal fun EmptyScreen(
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier.padding(horizontal = 16.dp)
) )
Text(text = buildAnnotatedString { Text(
append(stringResource(id = R.string.browser__more_info) + " ") text = buildAnnotatedString {
withStyle(style = SpanStyle(MaterialTheme.colors.primary)) { append(stringResource(id = R.string.browser__more_info) + " ")
append(stringResource(id = R.string.browser__more_info_link_title)) withStyle(style = SpanStyle(MaterialTheme.colors.primary)) {
} append(stringResource(id = R.string.browser__more_info_link_title))
}, style = MaterialTheme.typography.body2, modifier = Modifier }
.padding(horizontal = 16.dp) }, style = MaterialTheme.typography.body2, modifier = Modifier
.align(CenterHorizontally) .padding(horizontal = 16.dp)
.clickable { .align(CenterHorizontally)
activity?.openBrowserApp(url = "https://2fas.com/be") .clickable {
}, textAlign = TextAlign.Center) activity?.openBrowserApp(url = "https://2fas.com/be")
}, textAlign = TextAlign.Center
)
} }
Button(onClick = { viewModel.onPairBrowserClick() },
shape = ButtonShape(), Button(onClick = { onAddClick() }, shape = ButtonShape(), modifier = Modifier
modifier = Modifier .height(48.dp)
.height(48.dp) .constrainAs(pair) {
.constrainAs(pair) { bottom.linkTo(parent.bottom, margin = 16.dp)
bottom.linkTo(parent.bottom, margin = 16.dp) start.linkTo(parent.start)
start.linkTo(parent.start) end.linkTo(parent.end)
end.linkTo(parent.end) }) {
}) {
Text(text = stringResource(id = R.string.browser__pair_with_web_browser).uppercase(), color = ButtonTextColor()) Text(text = stringResource(id = R.string.browser__pair_with_web_browser).uppercase(), color = ButtonTextColor())
} }
} }

View File

@ -9,8 +9,6 @@ internal data class BrowserExtensionUiState(
val isLoading: Boolean = true, val isLoading: Boolean = true,
val pairedBrowsers: List<PairedBrowser> = emptyList(), val pairedBrowsers: List<PairedBrowser> = emptyList(),
val mobileDevice: MobileDevice? = null, val mobileDevice: MobileDevice? = null,
val isCameraPermissionGranted: Boolean = false,
val showRationaleDialog: Boolean = false,
val showEditDeviceDialog: Boolean = false, val showEditDeviceDialog: Boolean = false,
override val events: List<Event> = emptyList(), override val events: List<Event> = emptyList(),
) : UiState<BrowserExtensionUiState, BrowserExtensionUiState.Event> { ) : UiState<BrowserExtensionUiState, BrowserExtensionUiState.Event> {

View File

@ -7,17 +7,15 @@ import com.twofasapp.browserextension.domain.FetchPairedBrowsersCase
import com.twofasapp.browserextension.domain.ObserveMobileDeviceCase import com.twofasapp.browserextension.domain.ObserveMobileDeviceCase
import com.twofasapp.browserextension.domain.ObservePairedBrowsersCase import com.twofasapp.browserextension.domain.ObservePairedBrowsersCase
import com.twofasapp.browserextension.domain.UpdateMobileDeviceCase import com.twofasapp.browserextension.domain.UpdateMobileDeviceCase
import com.twofasapp.navigation.SettingsDirections import kotlinx.coroutines.flow.MutableStateFlow
import com.twofasapp.navigation.SettingsRouter import kotlinx.coroutines.flow.asStateFlow
import com.twofasapp.permissions.CameraPermissionRequestFlow import kotlinx.coroutines.flow.combine
import com.twofasapp.permissions.PermissionStatus import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
internal class BrowserExtensionViewModel( internal class BrowserExtensionViewModel(
private val dispatchers: Dispatchers, private val dispatchers: Dispatchers,
private val settingsRouter: SettingsRouter,
private val cameraPermissionRequest: CameraPermissionRequestFlow,
private val observeMobileDeviceCase: ObserveMobileDeviceCase, private val observeMobileDeviceCase: ObserveMobileDeviceCase,
private val observePairedBrowsersCase: ObservePairedBrowsersCase, private val observePairedBrowsersCase: ObservePairedBrowsersCase,
private val updateMobileDeviceCase: UpdateMobileDeviceCase, private val updateMobileDeviceCase: UpdateMobileDeviceCase,
@ -51,24 +49,6 @@ internal class BrowserExtensionViewModel(
} }
} }
fun onPairBrowserClick() {
cameraPermissionRequest.execute()
.take(1)
.onEach {
when (it) {
PermissionStatus.GRANTED -> settingsRouter.navigate(SettingsDirections.PairingScan)
PermissionStatus.DENIED -> Unit
PermissionStatus.DENIED_NEVER_ASK -> _uiState.update { state ->
state.copy(showRationaleDialog = true)
}
}
}.launchIn(viewModelScope)
}
fun onRationaleDialogDismiss() {
_uiState.update { it.copy(showRationaleDialog = false) }
}
fun onEditDeviceClick() { fun onEditDeviceClick() {
_uiState.update { it.copy(showEditDeviceDialog = true) } _uiState.update { it.copy(showEditDeviceDialog = true) }
} }

View File

@ -0,0 +1,121 @@
package com.twofasapp.browserextension.ui.main.permission
import android.Manifest
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import com.twofasapp.browserextension.ui.main.RequestPermission
import com.twofasapp.design.compose.ButtonHeight
import com.twofasapp.design.compose.ButtonShape
import com.twofasapp.design.compose.ButtonTextColor
import com.twofasapp.design.compose.Toolbar
import com.twofasapp.navigation.SettingsDirections
import com.twofasapp.navigation.SettingsRouter
import com.twofasapp.resources.R
import org.koin.androidx.compose.get
@Composable
internal fun BrowserExtensionPermissionScreen(
router: SettingsRouter = get(),
) {
// Launched only on >= TIRAMISU
var askForPermission by remember { mutableStateOf(false) }
Scaffold(
topBar = {
Toolbar(title = stringResource(id = R.string.browser__browser_extension)) {
router.navigate(SettingsDirections.GoBack)
}
}
) { padding ->
ConstraintLayout(
modifier = Modifier
.fillMaxHeight()
.padding(horizontal = 16.dp)
) {
val (content, pair) = createRefs()
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.constrainAs(content) {
top.linkTo(parent.top)
bottom.linkTo(pair.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
.padding(vertical = 16.dp)
) {
Image(
painter = painterResource(id = R.drawable.browser_extension_permission_image),
contentDescription = null,
modifier = Modifier
.height(130.dp)
.offset(y = (-16).dp)
)
Text(
text = stringResource(id = R.string.browser__push_notifications_title),
style = MaterialTheme.typography.h6,
textAlign = TextAlign.Center,
modifier = Modifier
)
Text(
text = stringResource(id = R.string.browser__push_notifications_content),
style = MaterialTheme.typography.body1,
textAlign = TextAlign.Center,
modifier = Modifier
)
}
Button(
onClick = { askForPermission = true },
shape = ButtonShape(),
modifier = Modifier
.height(ButtonHeight())
.constrainAs(pair) {
bottom.linkTo(parent.bottom, margin = 16.dp)
start.linkTo(parent.start)
end.linkTo(parent.end)
}) {
Text(text = stringResource(id = R.string.commons__continue).uppercase(), color = ButtonTextColor())
}
if (askForPermission) {
RequestPermission(
permission = Manifest.permission.POST_NOTIFICATIONS,
onGranted = { router.navigateBack() },
onDismiss = {
askForPermission = false
router.navigateBack()
},
rationaleTitle = stringResource(id = R.string.browser__push_notifications_title),
rationaleText = stringResource(id = R.string.browser__push_notifications_content),
)
}
}
}
}

View File

@ -0,0 +1,11 @@
package com.twofasapp.browserextension.ui.main.permission
import androidx.compose.runtime.Composable
class BrowserExtensionPermissionScreenFactory {
@Composable
fun create() {
BrowserExtensionPermissionScreen()
}
}

View File

@ -0,0 +1,28 @@
package com.twofasapp.browserextension.ui.main.permission
import androidx.lifecycle.viewModelScope
import com.twofasapp.base.BaseViewModel
import com.twofasapp.permissions.NotificationsPermissionRequestFlow
import com.twofasapp.permissions.PermissionStatus
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
internal class BrowserExtensionPermissionViewModel(
private val notificationsPermissionRequestFlow: NotificationsPermissionRequestFlow,
) : BaseViewModel() {
val permissionStatus = MutableStateFlow<PermissionStatus?>(null)
fun askForPermission() {
viewModelScope.launch {
notificationsPermissionRequestFlow.execute()
.take(1)
.collect { status ->
println("dupa: $status")
permissionStatus.update { status }
}
}
}
}

View File

@ -1,7 +1,14 @@
package com.twofasapp.browserextension.ui.pairing.progress package com.twofasapp.browserextension.ui.pairing.progress
import android.app.NotificationManager
import android.os.Build
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold import androidx.compose.material.Scaffold
@ -16,11 +23,19 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayout
import com.airbnb.lottie.compose.* import com.airbnb.lottie.compose.LottieAnimation
import com.twofasapp.resources.R import com.airbnb.lottie.compose.LottieCompositionSpec
import com.twofasapp.design.compose.* import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition
import com.twofasapp.design.compose.AnimatedContent
import com.twofasapp.design.compose.ButtonHeight
import com.twofasapp.design.compose.ButtonShape
import com.twofasapp.design.compose.ButtonTextColor
import com.twofasapp.design.compose.Toolbar
import com.twofasapp.navigation.SettingsDirections import com.twofasapp.navigation.SettingsDirections
import com.twofasapp.navigation.SettingsRouter import com.twofasapp.navigation.SettingsRouter
import com.twofasapp.resources.R
import org.koin.androidx.compose.get import org.koin.androidx.compose.get
@Composable @Composable
@ -28,6 +43,7 @@ internal fun PairingProgressScreen(
extensionId: String, extensionId: String,
viewModel: PairingProgressViewModel = get(), viewModel: PairingProgressViewModel = get(),
router: SettingsRouter = get(), router: SettingsRouter = get(),
notificationManager: NotificationManager = get()
) { ) {
val uiState = viewModel.uiState.collectAsState() val uiState = viewModel.uiState.collectAsState()
viewModel.pairBrowser(extensionId) viewModel.pairBrowser(extensionId)
@ -42,7 +58,7 @@ internal fun PairingProgressScreen(
AnimatedContent( AnimatedContent(
condition = uiState.value.isPairing, condition = uiState.value.isPairing,
contentWhenTrue = { ProgressContent() }, contentWhenTrue = { ProgressContent() },
contentWhenFalse = { ResultContent(uiState.value.isPairingSuccess, uiState.value.code, router) } contentWhenFalse = { ResultContent(uiState.value.isPairingSuccess, uiState.value.code, router, notificationManager) }
) )
} }
} }
@ -60,6 +76,7 @@ internal fun ResultContent(
isSuccess: Boolean, isSuccess: Boolean,
code: Int? = null, code: Int? = null,
router: SettingsRouter, router: SettingsRouter,
notificationManager: NotificationManager,
) { ) {
val image = if (isSuccess) R.drawable.browser_extension_success_image else R.drawable.browser_extension_error_image val image = if (isSuccess) R.drawable.browser_extension_success_image else R.drawable.browser_extension_error_image
@ -78,7 +95,15 @@ internal fun ResultContent(
val cta = if (isSuccess) R.string.commons__continue else R.string.browser__result_error_cta val cta = if (isSuccess) R.string.commons__continue else R.string.browser__result_error_cta
val ctaAction: () -> Unit = if (isSuccess) { val ctaAction: () -> Unit = if (isSuccess) {
{ router.navigate(SettingsDirections.GoBack) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (notificationManager.areNotificationsEnabled()) {
{ router.navigate(SettingsDirections.GoBack) }
} else {
{ router.navigate(SettingsDirections.Permission) }
}
} else {
{ router.navigate(SettingsDirections.GoBack) }
}
} else { } else {
{ router.navigate(SettingsDirections.PairingScan) } { router.navigate(SettingsDirections.PairingScan) }
} }

View File

@ -6,6 +6,7 @@ import com.twofasapp.base.dispatcher.Dispatchers
import com.twofasapp.browserextension.domain.PairBrowserCase import com.twofasapp.browserextension.domain.PairBrowserCase
import com.twofasapp.browserextension.domain.RegisterMobileDeviceCase import com.twofasapp.browserextension.domain.RegisterMobileDeviceCase
import com.twofasapp.network.exception.BrowserAlreadyPairedException import com.twofasapp.network.exception.BrowserAlreadyPairedException
import com.twofasapp.permissions.NotificationsPermissionRequestFlow
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -16,6 +17,7 @@ internal class PairingProgressViewModel(
private val dispatchers: Dispatchers, private val dispatchers: Dispatchers,
private val registerMobileDeviceCase: RegisterMobileDeviceCase, private val registerMobileDeviceCase: RegisterMobileDeviceCase,
private val pairBrowserCase: PairBrowserCase, private val pairBrowserCase: PairBrowserCase,
private val notificationsPermissionRequestFlow: NotificationsPermissionRequestFlow,
) : BaseViewModel() { ) : BaseViewModel() {
private val _uiState = MutableStateFlow(PairingProgressUiState()) private val _uiState = MutableStateFlow(PairingProgressUiState())

View File

@ -27,6 +27,7 @@ accompanistPlaceholder = { module = "com.google.accompanist:accompanist-placehol
accompanistSystemUi = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } accompanistSystemUi = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
accompanistPager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanist" } accompanistPager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanist" }
accompanistPagerIndicators = { module = "com.google.accompanist:accompanist-pager-indicators", version.ref = "accompanist" } accompanistPagerIndicators = { module = "com.google.accompanist:accompanist-pager-indicators", version.ref = "accompanist" }
accompanistPermissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
coil = { module = "io.coil-kt:coil", version.ref = "coil" } coil = { module = "io.coil-kt:coil", version.ref = "coil" }
coilCompose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } coilCompose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
composeActivity = { module = "androidx.activity:activity-compose", version.ref = "composeActivity" } composeActivity = { module = "androidx.activity:activity-compose", version.ref = "composeActivity" }
@ -153,6 +154,7 @@ accompanist = [
"accompanistSystemUi", "accompanistSystemUi",
"accompanistPager", "accompanistPager",
"accompanistPagerIndicators", "accompanistPagerIndicators",
"accompanistPermissions"
] ]
coil = [ coil = [
"coil", "coil",

View File

@ -1,5 +1,7 @@
package com.twofasapp.navigation package com.twofasapp.navigation
import android.os.Build
import androidx.annotation.RequiresApi
import com.twofasapp.navigation.base.Directions import com.twofasapp.navigation.base.Directions
sealed interface SettingsDirections : Directions { sealed interface SettingsDirections : Directions {
@ -9,5 +11,6 @@ sealed interface SettingsDirections : Directions {
object BrowserExtension : SettingsDirections object BrowserExtension : SettingsDirections
class BrowserDetails(val extensionId: String) : SettingsDirections class BrowserDetails(val extensionId: String) : SettingsDirections
object PairingScan : SettingsDirections object PairingScan : SettingsDirections
object Permission : SettingsDirections
class PairingProgress(val extensionId: String) : SettingsDirections class PairingProgress(val extensionId: String) : SettingsDirections
} }

View File

@ -0,0 +1,14 @@
package com.twofasapp.permissions
import android.Manifest
import android.app.Activity
import android.os.Build
import androidx.annotation.RequiresApi
import com.twofasapp.permissions.internal.PermissionRequest
import com.twofasapp.permissions.internal.PermissionRequestFlow
class NotificationsPermissionRequestFlow(activity: Activity) : PermissionRequestFlow(activity) {
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override val permission: String = Manifest.permission.POST_NOTIFICATIONS
}

View File

@ -0,0 +1,117 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="560dp"
android:height="400dp"
android:viewportWidth="560"
android:viewportHeight="400">
<path
android:pathData="M475.5,22H155.5C143.07,22 133,32.07 133,44.5C133,56.93 143.07,67 155.5,67H475.5C487.93,67 498,56.93 498,44.5C498,32.07 487.93,22 475.5,22Z"
android:fillColor="#A50006"/>
<path
android:pathData="M27,375V87C27,60.49 48.49,39 75,39H415C441.51,39 463,60.49 463,87V132.5"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M463,377.5V312"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M379,83.67H136.67C121.94,83.67 110,71.73 110,57"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M245,67C247.76,67 250,64.76 250,62C250,59.24 247.76,57 245,57C242.24,57 240,59.24 240,62C240,64.76 242.24,67 245,67Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M490.49,235H242.1C232.66,235 225,242.65 225,252.1C225,261.54 232.66,269.19 242.1,269.19H490.49C499.93,269.19 507.59,261.54 507.59,252.1C507.59,242.65 499.93,235 490.49,235Z"
android:fillColor="#FF8095"/>
<path
android:pathData="M210,184.37C210,183.15 210.94,182.17 212.16,182.17C213.38,182.17 214.36,183.14 214.36,184.37V185.31C215.58,183.55 217.35,182 220.3,182C224.58,182 227.07,184.88 227.07,189.27V199.42C227.07,200.64 226.13,201.58 224.91,201.58C223.69,201.58 222.71,200.64 222.71,199.42V190.6C222.71,187.65 221.24,185.96 218.64,185.96C216.04,185.96 214.36,187.72 214.36,190.68V199.43C214.36,200.65 213.39,201.59 212.16,201.59C210.93,201.59 210,200.65 210,199.43V184.38V184.37Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M231,192V191.93C231,186.49 235.32,182 241.15,182C246.98,182 251.27,186.43 251.27,191.86V191.93C251.27,197.33 246.95,201.83 241.08,201.83C235.21,201.83 231,197.4 231,192ZM246.91,192V191.93C246.91,188.58 244.5,185.81 241.08,185.81C237.66,185.81 235.36,188.55 235.36,191.86V191.93C235.36,195.24 237.77,198.01 241.15,198.01C244.53,198.01 246.91,195.27 246.91,192Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M256.59,196.15V186.14H255.87C254.83,186.14 254,185.31 254,184.27C254,183.23 254.83,182.4 255.87,182.4H256.59V179.16C256.59,177.97 257.56,177 258.79,177C260.02,177 260.95,177.97 260.95,179.16V182.4H264.37C265.41,182.4 266.28,183.23 266.28,184.27C266.28,185.31 265.42,186.14 264.37,186.14H260.95V195.46C260.95,197.15 261.81,197.84 263.29,197.84C263.79,197.84 264.23,197.73 264.37,197.73C265.34,197.73 266.2,198.52 266.2,199.53C266.2,200.32 265.66,200.97 265.05,201.22C264.11,201.54 263.21,201.72 262.06,201.72C258.86,201.72 256.59,200.32 256.59,196.14V196.15Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M271,177.12C271,175.82 272.08,175 273.48,175C274.88,175 275.96,175.83 275.96,177.12V177.34C275.96,178.64 274.88,179.5 273.48,179.5C272.08,179.5 271,178.64 271,177.34V177.12ZM271.32,184.21C271.32,182.99 272.26,182.01 273.48,182.01C274.7,182.01 275.68,182.98 275.68,184.21V199.26C275.68,200.48 274.71,201.42 273.48,201.42C272.25,201.42 271.32,200.48 271.32,199.26V184.21Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M281.6,186.23H280.84C279.83,186.23 279,185.44 279,184.43C279,183.42 279.83,182.59 280.84,182.59H281.6V181.19C281.6,179.1 282.14,177.52 283.15,176.51C284.16,175.5 285.56,175 287.43,175C288.3,175 289.01,175.07 289.63,175.18C290.42,175.29 291.14,176.01 291.14,176.98C291.14,177.95 290.31,178.82 289.31,178.78C289.06,178.74 288.7,178.71 288.41,178.71C286.75,178.71 285.89,179.61 285.89,181.59V182.63H289.27C290.31,182.63 291.11,183.42 291.11,184.43C291.11,185.44 290.28,186.23 289.27,186.23H285.96V199.51C285.96,200.7 284.99,201.67 283.76,201.67C282.53,201.67 281.6,200.7 281.6,199.51V186.23ZM294.09,177.38C294.09,176.08 295.17,175.26 296.57,175.26C297.97,175.26 299.05,176.09 299.05,177.38V177.6C299.05,178.9 297.97,179.76 296.57,179.76C295.17,179.76 294.09,178.9 294.09,177.6V177.38ZM294.41,184.47C294.41,183.25 295.35,182.27 296.57,182.27C297.79,182.27 298.77,183.24 298.77,184.47V199.52C298.77,200.74 297.8,201.68 296.57,201.68C295.34,201.68 294.41,200.74 294.41,199.52V184.47Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M304,192V191.93C304,186.53 308.17,182 313.9,182C316.89,182 318.94,182.97 320.56,184.38C320.85,184.63 321.24,185.17 321.24,185.89C321.24,187.01 320.34,187.87 319.22,187.87C318.68,187.87 318.21,187.65 317.93,187.44C316.81,186.5 315.59,185.82 313.86,185.82C310.69,185.82 308.35,188.56 308.35,191.87V191.94C308.35,195.32 310.69,198.02 314.04,198.02C315.77,198.02 317.1,197.34 318.29,196.33C318.54,196.11 318.97,195.86 319.48,195.86C320.52,195.86 321.35,196.72 321.35,197.77C321.35,198.35 321.13,198.81 320.74,199.14C319.05,200.76 317,201.84 313.83,201.84C308.18,201.84 304,197.41 304,192.01V192Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M324,195.79V195.72C324,191.62 327.2,189.6 331.85,189.6C333.97,189.6 335.49,189.92 336.96,190.39V189.92C336.96,187.22 335.3,185.78 332.25,185.78C330.59,185.78 329.23,186.07 328.04,186.54C327.79,186.61 327.57,186.65 327.36,186.65C326.35,186.65 325.53,185.86 325.53,184.85C325.53,184.06 326.07,183.37 326.72,183.12C328.52,182.44 330.35,182 332.84,182C335.68,182 337.81,182.76 339.14,184.12C340.54,185.49 341.19,187.5 341.19,189.99V199.35C341.19,200.54 340.25,201.44 339.07,201.44C337.81,201.44 336.95,200.58 336.95,199.6V198.88C335.65,200.43 333.67,201.65 330.76,201.65C327.2,201.65 324.03,199.6 324.03,195.78L324,195.79ZM337.03,194.42V193.12C335.91,192.69 334.44,192.36 332.71,192.36C329.9,192.36 328.25,193.55 328.25,195.53V195.6C328.25,197.44 329.87,198.48 331.96,198.48C334.84,198.48 337.04,196.82 337.04,194.41L337.03,194.42Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M347.59,196.15V186.14H346.87C345.83,186.14 345,185.31 345,184.27C345,183.23 345.83,182.4 346.87,182.4H347.59V179.16C347.59,177.97 348.56,177 349.79,177C351.02,177 351.95,177.97 351.95,179.16V182.4H355.37C356.41,182.4 357.28,183.23 357.28,184.27C357.28,185.31 356.42,186.14 355.37,186.14H351.95V195.46C351.95,197.15 352.81,197.84 354.29,197.84C354.79,197.84 355.23,197.73 355.37,197.73C356.34,197.73 357.21,198.52 357.21,199.53C357.21,200.32 356.67,200.97 356.06,201.22C355.12,201.54 354.23,201.72 353.07,201.72C349.87,201.72 347.6,200.32 347.6,196.14L347.59,196.15Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M361,177.12C361,175.82 362.08,175 363.48,175C364.88,175 365.96,175.83 365.96,177.12V177.34C365.96,178.64 364.88,179.5 363.48,179.5C362.08,179.5 361,178.64 361,177.34V177.12ZM361.32,184.21C361.32,182.99 362.26,182.01 363.48,182.01C364.7,182.01 365.68,182.98 365.68,184.21V199.26C365.68,200.48 364.71,201.42 363.48,201.42C362.25,201.42 361.32,200.48 361.32,199.26V184.21Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M370.01,192V191.93C370.01,186.49 374.33,182 380.16,182C385.99,182 390.27,186.43 390.27,191.86V191.93C390.27,197.33 385.95,201.83 380.08,201.83C374.21,201.83 370,197.4 370,192H370.01ZM385.92,192V191.93C385.92,188.58 383.51,185.81 380.09,185.81C376.67,185.81 374.37,188.55 374.37,191.86V191.93C374.37,195.24 376.78,198.01 380.16,198.01C383.54,198.01 385.92,195.27 385.92,192Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M395,184.37C395,183.15 395.94,182.17 397.16,182.17C398.38,182.17 399.36,183.14 399.36,184.37V185.31C400.58,183.55 402.35,182 405.3,182C409.58,182 412.07,184.88 412.07,189.27V199.42C412.07,200.64 411.13,201.58 409.91,201.58C408.69,201.58 407.71,200.64 407.71,199.42V190.6C407.71,187.65 406.24,185.96 403.64,185.96C401.04,185.96 399.36,187.72 399.36,190.68V199.43C399.36,200.65 398.39,201.59 397.16,201.59C395.93,201.59 395,200.65 395,199.43V184.38V184.37Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M102.64,150H501C518.67,150 533,164.33 533,182V257C533,274.67 518.67,289 501,289H83C65.33,289 51,274.67 51,257V182C51,178 51.73,174.18 53.07,170.65"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M172.04,283.55L148.02,301.25C143.63,304.49 137.63,304.49 133.24,301.25L109.22,283.55C100.28,276.96 95,266.52 95,255.41V202.1C95,198.61 97.26,195.52 100.6,194.47L121.02,188.02C133.79,183.99 147.48,183.99 160.25,188.02L180.67,194.47C184,195.52 186.27,198.61 186.27,202.1V255.41C186.27,266.52 180.99,276.96 172.05,283.55H172.04Z"
android:fillColor="#ED1B24"/>
<path
android:pathData="M154.01,229C154.01,221.84 148.22,216.03 141.07,216C133.87,215.97 128,221.81 128,229C128,233.78 130.58,237.96 134.43,240.22C135.05,240.58 135.37,241.3 135.2,241.99L132.02,255.38C131.78,256.39 132.55,257.37 133.59,257.37H148.42C149.46,257.37 150.23,256.4 149.99,255.38L146.81,241.99C146.64,241.29 146.96,240.58 147.58,240.22C151.42,237.96 154,233.79 154,229.01L154.01,229Z"
android:fillColor="#ED1B24"/>
<path
android:pathData="M145.89,229.45C145.89,221.5 139.46,215.04 131.52,215C123.52,214.96 117,221.45 117,229.45C117,234.76 119.87,239.4 124.14,241.91C124.83,242.31 125.18,243.11 125,243.88L121.47,258.76C121.2,259.89 122.06,260.97 123.21,260.97H139.69C140.85,260.97 141.7,259.89 141.43,258.76L137.9,243.88C137.72,243.11 138.07,242.31 138.76,241.91C143.03,239.4 145.9,234.76 145.9,229.45H145.89Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M160.04,277.55L136.02,295.25C131.63,298.49 125.63,298.49 121.24,295.25L97.22,277.55C88.28,270.96 83,260.52 83,249.41V196.1C83,192.61 85.26,189.52 88.6,188.47L109.02,182.02C121.79,177.99 135.48,177.99 148.25,182.02L168.67,188.47C172,189.52 174.27,192.61 174.27,196.1V249.41C174.27,260.52 168.99,270.96 160.05,277.55H160.04Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M142.01,223C142.01,215.84 136.22,210.03 129.07,210C121.92,209.97 116,215.81 116,223C116,227.78 118.58,231.96 122.43,234.22C123.05,234.58 123.37,235.3 123.2,235.99L120.02,249.38C119.78,250.39 120.55,251.37 121.59,251.37H136.42C137.46,251.37 138.23,250.4 137.99,249.38L134.81,235.99C134.64,235.29 134.96,234.58 135.58,234.22C139.42,231.96 142,227.79 142,223.01L142.01,223Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M505.59,219H226.1C221.38,219 217.11,220.91 214.01,224.01C210.92,227.1 209,231.38 209,236.1C209,242.28 212.28,247.7 217.2,250.7C219.79,252.29 222.84,253.2 226.09,253.2H329.75H328.58"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M486,311.67H76.67C61.94,311.67 50,299.73 50,285"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,117 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="560dp"
android:height="400dp"
android:viewportWidth="560"
android:viewportHeight="400">
<path
android:pathData="M475.5,22H155.5C143.07,22 133,32.07 133,44.5C133,56.93 143.07,67 155.5,67H475.5C487.93,67 498,56.93 498,44.5C498,32.07 487.93,22 475.5,22Z"
android:fillColor="#A50006"/>
<path
android:pathData="M27,375V87C27,60.49 48.49,39 75,39H415C441.51,39 463,60.49 463,87V132.5"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M463,377.5V312"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M379,83.67H136.67C121.94,83.67 110,71.73 110,57"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M245,67C247.76,67 250,64.76 250,62C250,59.24 247.76,57 245,57C242.24,57 240,59.24 240,62C240,64.76 242.24,67 245,67Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M490.49,235H242.1C232.66,235 225,242.65 225,252.1C225,261.54 232.66,269.19 242.1,269.19H490.49C499.93,269.19 507.59,261.54 507.59,252.1C507.59,242.65 499.93,235 490.49,235Z"
android:fillColor="#FF8095"/>
<path
android:pathData="M210,184.37C210,183.15 210.94,182.17 212.16,182.17C213.38,182.17 214.36,183.14 214.36,184.37V185.31C215.58,183.55 217.35,182 220.3,182C224.58,182 227.07,184.88 227.07,189.27V199.42C227.07,200.64 226.13,201.58 224.91,201.58C223.69,201.58 222.71,200.64 222.71,199.42V190.6C222.71,187.65 221.24,185.96 218.64,185.96C216.04,185.96 214.36,187.72 214.36,190.68V199.43C214.36,200.65 213.39,201.59 212.16,201.59C210.93,201.59 210,200.65 210,199.43V184.38V184.37Z"
android:fillColor="#000000"/>
<path
android:pathData="M231,192V191.93C231,186.49 235.32,182 241.15,182C246.98,182 251.27,186.43 251.27,191.86V191.93C251.27,197.33 246.95,201.83 241.08,201.83C235.21,201.83 231,197.4 231,192ZM246.91,192V191.93C246.91,188.58 244.5,185.81 241.08,185.81C237.66,185.81 235.36,188.55 235.36,191.86V191.93C235.36,195.24 237.77,198.01 241.15,198.01C244.53,198.01 246.91,195.27 246.91,192Z"
android:fillColor="#000000"/>
<path
android:pathData="M256.59,196.15V186.14H255.87C254.83,186.14 254,185.31 254,184.27C254,183.23 254.83,182.4 255.87,182.4H256.59V179.16C256.59,177.97 257.56,177 258.79,177C260.02,177 260.95,177.97 260.95,179.16V182.4H264.37C265.41,182.4 266.28,183.23 266.28,184.27C266.28,185.31 265.42,186.14 264.37,186.14H260.95V195.46C260.95,197.15 261.81,197.84 263.29,197.84C263.79,197.84 264.23,197.73 264.37,197.73C265.34,197.73 266.2,198.52 266.2,199.53C266.2,200.32 265.66,200.97 265.05,201.22C264.11,201.54 263.21,201.72 262.06,201.72C258.86,201.72 256.59,200.32 256.59,196.14V196.15Z"
android:fillColor="#000000"/>
<path
android:pathData="M271,177.12C271,175.82 272.08,175 273.48,175C274.88,175 275.96,175.83 275.96,177.12V177.34C275.96,178.64 274.88,179.5 273.48,179.5C272.08,179.5 271,178.64 271,177.34V177.12ZM271.32,184.21C271.32,182.99 272.26,182.01 273.48,182.01C274.7,182.01 275.68,182.98 275.68,184.21V199.26C275.68,200.48 274.71,201.42 273.48,201.42C272.25,201.42 271.32,200.48 271.32,199.26V184.21Z"
android:fillColor="#000000"/>
<path
android:pathData="M281.6,186.23H280.84C279.83,186.23 279,185.44 279,184.43C279,183.42 279.83,182.59 280.84,182.59H281.6V181.19C281.6,179.1 282.14,177.52 283.15,176.51C284.16,175.5 285.56,175 287.43,175C288.3,175 289.01,175.07 289.63,175.18C290.42,175.29 291.14,176.01 291.14,176.98C291.14,177.95 290.31,178.82 289.31,178.78C289.06,178.74 288.7,178.71 288.41,178.71C286.75,178.71 285.89,179.61 285.89,181.59V182.63H289.27C290.31,182.63 291.11,183.42 291.11,184.43C291.11,185.44 290.28,186.23 289.27,186.23H285.96V199.51C285.96,200.7 284.99,201.67 283.76,201.67C282.53,201.67 281.6,200.7 281.6,199.51V186.23ZM294.09,177.38C294.09,176.08 295.17,175.26 296.57,175.26C297.97,175.26 299.05,176.09 299.05,177.38V177.6C299.05,178.9 297.97,179.76 296.57,179.76C295.17,179.76 294.09,178.9 294.09,177.6V177.38ZM294.41,184.47C294.41,183.25 295.35,182.27 296.57,182.27C297.79,182.27 298.77,183.24 298.77,184.47V199.52C298.77,200.74 297.8,201.68 296.57,201.68C295.34,201.68 294.41,200.74 294.41,199.52V184.47Z"
android:fillColor="#000000"/>
<path
android:pathData="M304,192V191.93C304,186.53 308.17,182 313.9,182C316.89,182 318.94,182.97 320.56,184.38C320.85,184.63 321.24,185.17 321.24,185.89C321.24,187.01 320.34,187.87 319.22,187.87C318.68,187.87 318.21,187.65 317.93,187.44C316.81,186.5 315.59,185.82 313.86,185.82C310.69,185.82 308.35,188.56 308.35,191.87V191.94C308.35,195.32 310.69,198.02 314.04,198.02C315.77,198.02 317.1,197.34 318.29,196.33C318.54,196.11 318.97,195.86 319.48,195.86C320.52,195.86 321.35,196.72 321.35,197.77C321.35,198.35 321.13,198.81 320.74,199.14C319.05,200.76 317,201.84 313.83,201.84C308.18,201.84 304,197.41 304,192.01V192Z"
android:fillColor="#000000"/>
<path
android:pathData="M324,195.79V195.72C324,191.62 327.2,189.6 331.85,189.6C333.97,189.6 335.49,189.92 336.96,190.39V189.92C336.96,187.22 335.3,185.78 332.25,185.78C330.59,185.78 329.23,186.07 328.04,186.54C327.79,186.61 327.57,186.65 327.36,186.65C326.35,186.65 325.53,185.86 325.53,184.85C325.53,184.06 326.07,183.37 326.72,183.12C328.52,182.44 330.35,182 332.84,182C335.68,182 337.81,182.76 339.14,184.12C340.54,185.49 341.19,187.5 341.19,189.99V199.35C341.19,200.54 340.25,201.44 339.07,201.44C337.81,201.44 336.95,200.58 336.95,199.6V198.88C335.65,200.43 333.67,201.65 330.76,201.65C327.2,201.65 324.03,199.6 324.03,195.78L324,195.79ZM337.03,194.42V193.12C335.91,192.69 334.44,192.36 332.71,192.36C329.9,192.36 328.25,193.55 328.25,195.53V195.6C328.25,197.44 329.87,198.48 331.96,198.48C334.84,198.48 337.04,196.82 337.04,194.41L337.03,194.42Z"
android:fillColor="#000000"/>
<path
android:pathData="M347.59,196.15V186.14H346.87C345.83,186.14 345,185.31 345,184.27C345,183.23 345.83,182.4 346.87,182.4H347.59V179.16C347.59,177.97 348.56,177 349.79,177C351.02,177 351.95,177.97 351.95,179.16V182.4H355.37C356.41,182.4 357.28,183.23 357.28,184.27C357.28,185.31 356.42,186.14 355.37,186.14H351.95V195.46C351.95,197.15 352.81,197.84 354.29,197.84C354.79,197.84 355.23,197.73 355.37,197.73C356.34,197.73 357.21,198.52 357.21,199.53C357.21,200.32 356.67,200.97 356.06,201.22C355.12,201.54 354.23,201.72 353.07,201.72C349.87,201.72 347.6,200.32 347.6,196.14L347.59,196.15Z"
android:fillColor="#000000"/>
<path
android:pathData="M361,177.12C361,175.82 362.08,175 363.48,175C364.88,175 365.96,175.83 365.96,177.12V177.34C365.96,178.64 364.88,179.5 363.48,179.5C362.08,179.5 361,178.64 361,177.34V177.12ZM361.32,184.21C361.32,182.99 362.26,182.01 363.48,182.01C364.7,182.01 365.68,182.98 365.68,184.21V199.26C365.68,200.48 364.71,201.42 363.48,201.42C362.25,201.42 361.32,200.48 361.32,199.26V184.21Z"
android:fillColor="#000000"/>
<path
android:pathData="M370.01,192V191.93C370.01,186.49 374.33,182 380.16,182C385.99,182 390.27,186.43 390.27,191.86V191.93C390.27,197.33 385.95,201.83 380.08,201.83C374.21,201.83 370,197.4 370,192H370.01ZM385.92,192V191.93C385.92,188.58 383.51,185.81 380.09,185.81C376.67,185.81 374.37,188.55 374.37,191.86V191.93C374.37,195.24 376.78,198.01 380.16,198.01C383.54,198.01 385.92,195.27 385.92,192Z"
android:fillColor="#000000"/>
<path
android:pathData="M395,184.37C395,183.15 395.94,182.17 397.16,182.17C398.38,182.17 399.36,183.14 399.36,184.37V185.31C400.58,183.55 402.35,182 405.3,182C409.58,182 412.07,184.88 412.07,189.27V199.42C412.07,200.64 411.13,201.58 409.91,201.58C408.69,201.58 407.71,200.64 407.71,199.42V190.6C407.71,187.65 406.24,185.96 403.64,185.96C401.04,185.96 399.36,187.72 399.36,190.68V199.43C399.36,200.65 398.39,201.59 397.16,201.59C395.93,201.59 395,200.65 395,199.43V184.38V184.37Z"
android:fillColor="#000000"/>
<path
android:pathData="M102.64,150H501C518.67,150 533,164.33 533,182V257C533,274.67 518.67,289 501,289H83C65.33,289 51,274.67 51,257V182C51,178 51.73,174.18 53.07,170.65"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M172.04,283.55L148.02,301.25C143.63,304.49 137.63,304.49 133.24,301.25L109.22,283.55C100.28,276.96 95,266.52 95,255.41V202.1C95,198.61 97.26,195.52 100.6,194.47L121.02,188.02C133.79,183.99 147.48,183.99 160.25,188.02L180.67,194.47C184,195.52 186.27,198.61 186.27,202.1V255.41C186.27,266.52 180.99,276.96 172.05,283.55H172.04Z"
android:fillColor="#ED1B24"/>
<path
android:pathData="M154.01,229C154.01,221.84 148.22,216.03 141.07,216C133.87,215.97 128,221.81 128,229C128,233.78 130.58,237.96 134.43,240.22C135.05,240.58 135.37,241.3 135.2,241.99L132.02,255.38C131.78,256.39 132.55,257.37 133.59,257.37H148.42C149.46,257.37 150.23,256.4 149.99,255.38L146.81,241.99C146.64,241.29 146.96,240.58 147.58,240.22C151.42,237.96 154,233.79 154,229.01L154.01,229Z"
android:fillColor="#ED1B24"/>
<path
android:pathData="M145.89,229.45C145.89,221.5 139.46,215.04 131.52,215C123.52,214.96 117,221.45 117,229.45C117,234.76 119.87,239.4 124.14,241.91C124.83,242.31 125.18,243.11 125,243.88L121.47,258.76C121.2,259.89 122.06,260.97 123.21,260.97H139.69C140.85,260.97 141.7,259.89 141.43,258.76L137.9,243.88C137.72,243.11 138.07,242.31 138.76,241.91C143.03,239.4 145.9,234.76 145.9,229.45H145.89Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M160.04,277.55L136.02,295.25C131.63,298.49 125.63,298.49 121.24,295.25L97.22,277.55C88.28,270.96 83,260.52 83,249.41V196.1C83,192.61 85.26,189.52 88.6,188.47L109.02,182.02C121.79,177.99 135.48,177.99 148.25,182.02L168.67,188.47C172,189.52 174.27,192.61 174.27,196.1V249.41C174.27,260.52 168.99,270.96 160.05,277.55H160.04Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M142.01,223C142.01,215.84 136.22,210.03 129.07,210C121.92,209.97 116,215.81 116,223C116,227.78 118.58,231.96 122.43,234.22C123.05,234.58 123.37,235.3 123.2,235.99L120.02,249.38C119.78,250.39 120.55,251.37 121.59,251.37H136.42C137.46,251.37 138.23,250.4 137.99,249.38L134.81,235.99C134.64,235.29 134.96,234.58 135.58,234.22C139.42,231.96 142,227.79 142,223.01L142.01,223Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M505.59,219H226.1C221.38,219 217.11,220.91 214.01,224.01C210.92,227.1 209,231.38 209,236.1C209,242.28 212.28,247.7 217.2,250.7C219.79,252.29 222.84,253.2 226.09,253.2H329.75H328.58"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M486,311.67H76.67C61.94,311.67 50,299.73 50,285"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</vector>