summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorAlbin <albin@mullvad.net>2022-06-17 08:28:03 +0200
committerAlbin <albin@mullvad.net>2022-06-22 11:57:30 +0200
commite87aea2ce5baa7a9c7466e345dad7bc0739445a6 (patch)
tree5bd630b64252f4de2f334269d9afe1af214c6e63 /android
parent319c524f55729a14c62bb6d8a8985e4d87030fb6 (diff)
downloadmullvadvpn-e87aea2ce5baa7a9c7466e345dad7bc0739445a6.tar.xz
mullvadvpn-e87aea2ce5baa7a9c7466e345dad7bc0739445a6.zip
Add device list ui
Diffstat (limited to 'android')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Dialog.kt125
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/HtmlText.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ReadOnlyComposables.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt176
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceRevokedScreen.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeviceListUiState.kt19
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragments/DeviceListFragment.kt76
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt50
-rw-r--r--android/app/src/main/res/values/colors.xml1
10 files changed, 457 insertions, 9 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Dialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Dialog.kt
new file mode 100644
index 0000000000..fdadbd5b29
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Dialog.kt
@@ -0,0 +1,125 @@
+package net.mullvad.mullvadvpn.compose.component
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.AlertDialog
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusOrder
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.model.Device
+import net.mullvad.mullvadvpn.util.capitalizeFirstCharOfEachWord
+import net.mullvad.mullvadvpn.viewmodel.DeviceListViewModel
+
+@Composable
+fun ShowDeviceRemovalDialog(viewModel: DeviceListViewModel, device: Device) {
+ AlertDialog(
+ onDismissRequest = {
+ viewModel.clearStagedDevice()
+ },
+ title = {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .padding(top = 0.dp)
+ .fillMaxWidth()
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.icon_alert),
+ contentDescription = "Remove",
+ modifier = Modifier
+ .width(50.dp)
+ .height(50.dp)
+ )
+ }
+ },
+ text = {
+ val htmlFormattedDialogText = textResource(
+ id = R.string.max_devices_confirm_removal_description,
+ device.name.capitalizeFirstCharOfEachWord()
+ ).let { introText ->
+ if (device.ports.isNotEmpty()) {
+ introText.plus(" " + stringResource(id = R.string.port_removal_notice))
+ } else {
+ introText
+ }
+ }
+
+ HtmlText(
+ htmlFormattedString = htmlFormattedDialogText,
+ textSize = 16.sp.value
+ )
+ },
+ buttons = {
+ Column(
+ Modifier
+ .padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
+ ) {
+ Button(
+ modifier = Modifier
+ .height(dimensionResource(id = R.dimen.button_height))
+ .defaultMinSize(
+ minWidth = 0.dp,
+ minHeight = dimensionResource(id = R.dimen.button_height)
+ )
+ .fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(
+ backgroundColor = colorResource(id = R.color.red),
+ contentColor = Color.White
+ ),
+ onClick = {
+ viewModel.confirmRemoval()
+ }
+ ) {
+ Text(
+ text = stringResource(id = R.string.confirm_removal),
+ fontSize = 18.sp
+ )
+ }
+ Button(
+ contentPadding = PaddingValues(0.dp),
+ modifier = Modifier
+ .focusOrder(FocusRequester())
+ .padding(top = 16.dp)
+ .height(dimensionResource(id = R.dimen.button_height))
+ .defaultMinSize(
+ minWidth = 0.dp,
+ minHeight = dimensionResource(id = R.dimen.button_height)
+ )
+ .fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(
+ backgroundColor = colorResource(id = R.color.blue),
+ contentColor = Color.White
+ ),
+ onClick = {
+ viewModel.clearStagedDevice()
+ }
+ ) {
+ Text(
+ text = stringResource(id = R.string.back),
+ fontSize = 18.sp
+ )
+ }
+ }
+ },
+ backgroundColor = colorResource(id = R.color.darkBlue)
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/HtmlText.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/HtmlText.kt
index 81d212df84..545d724228 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/HtmlText.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/HtmlText.kt
@@ -1,15 +1,15 @@
package net.mullvad.mullvadvpn.compose.component
-import android.text.Spanned
import android.util.TypedValue
import android.widget.TextView
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.text.HtmlCompat
@Composable
fun HtmlText(
- htmlFormattedText: Spanned,
+ htmlFormattedString: String,
textSize: Float,
modifier: Modifier = Modifier
) {
@@ -20,6 +20,8 @@ fun HtmlText(
setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize)
}
},
- update = { it.text = htmlFormattedText }
+ update = {
+ it.text = HtmlCompat.fromHtml(htmlFormattedString, HtmlCompat.FROM_HTML_MODE_COMPACT)
+ }
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ReadOnlyComposables.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ReadOnlyComposables.kt
index af685e5e5c..8fe7d44c75 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ReadOnlyComposables.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ReadOnlyComposables.kt
@@ -1,16 +1,12 @@
package net.mullvad.mullvadvpn.compose.component
-import android.text.Spanned
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.platform.LocalContext
-import androidx.core.text.HtmlCompat
@Composable
@ReadOnlyComposable
-fun textResource(@StringRes id: Int, vararg formatArgs: Any): Spanned {
- return LocalContext.current.resources.getString(id, *formatArgs).let { text ->
- HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_COMPACT)
- }
+fun textResource(@StringRes id: Int, vararg formatArgs: Any): String {
+ return LocalContext.current.resources.getString(id, *formatArgs)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt
new file mode 100644
index 0000000000..b9dcae7e8e
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt
@@ -0,0 +1,176 @@
+package net.mullvad.mullvadvpn.compose.screen
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.constraintlayout.compose.ConstraintLayout
+import androidx.constraintlayout.compose.Dimension
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.component.ActionButton
+import net.mullvad.mullvadvpn.compose.component.ItemList
+import net.mullvad.mullvadvpn.compose.component.ShowDeviceRemovalDialog
+import net.mullvad.mullvadvpn.util.capitalizeFirstCharOfEachWord
+import net.mullvad.mullvadvpn.viewmodel.DeviceListViewModel
+
+@Composable
+fun DeviceListScreen(
+ viewModel: DeviceListViewModel,
+ onBackClick: () -> Unit,
+ onContinueWithLogin: () -> Unit
+) {
+ val state = viewModel.uiState.collectAsState().value
+
+ if (state.deviceStagedForRemoval != null) {
+ ShowDeviceRemovalDialog(
+ viewModel = viewModel,
+ device = state.deviceStagedForRemoval
+ )
+ }
+
+ ConstraintLayout(
+ modifier = Modifier
+ .fillMaxHeight()
+ .fillMaxWidth()
+ .background(colorResource(id = R.color.darkBlue))
+ ) {
+ val (icon, message, list, actionButtons) = createRefs()
+
+ Image(
+ painter = painterResource(
+ id = if (state.hasTooManyDevices) {
+ R.drawable.icon_fail
+ } else {
+ R.drawable.icon_success
+ }
+ ),
+ contentDescription = null, // No meaningful user info or action.
+ modifier = Modifier
+ .constrainAs(icon) {
+ top.linkTo(parent.top, margin = 30.dp)
+ start.linkTo(parent.start)
+ end.linkTo(parent.end)
+ }
+ .width(64.dp)
+ .height(64.dp)
+ )
+
+ Column(
+ modifier = Modifier
+ .constrainAs(message) {
+ top.linkTo(icon.bottom, margin = 16.dp)
+ start.linkTo(parent.start, margin = 22.dp)
+ end.linkTo(parent.end, margin = 22.dp)
+ width = Dimension.fillToConstraints
+ },
+ ) {
+ Text(
+ text = stringResource(
+ id = if (state.hasTooManyDevices) {
+ R.string.max_devices_warning_title
+ } else {
+ R.string.max_devices_resolved_title
+ }
+ ),
+ fontSize = 24.sp,
+ color = Color.White,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ text = stringResource(
+ id = if (state.hasTooManyDevices) {
+ R.string.max_devices_warning_description
+ } else {
+ R.string.max_devices_resolved_description
+ }
+ ),
+ color = Color.White,
+ fontSize = 14.sp,
+ modifier = Modifier
+ .wrapContentHeight()
+ .animateContentSize()
+ .padding(top = 8.dp)
+ )
+ }
+
+ Box(
+ modifier = Modifier
+ .constrainAs(list) {
+ top.linkTo(message.bottom, margin = 20.dp)
+ bottom.linkTo(actionButtons.top, margin = 5.dp)
+ height = Dimension.fillToConstraints
+ width = Dimension.matchParent
+ }
+ ) {
+ if (state.isLoading) {
+ CircularProgressIndicator(
+ color = Color.White,
+ strokeWidth = 8.dp,
+ modifier = Modifier.align(Alignment.Center)
+ )
+ } else {
+ ItemList(
+ state.devices,
+ itemText = { it.name.capitalizeFirstCharOfEachWord() },
+ onItemClicked = {
+ viewModel.stageDeviceForRemoval(it)
+ },
+ itemPainter = painterResource(id = R.drawable.icon_close)
+ )
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .constrainAs(actionButtons) {
+ bottom.linkTo(parent.bottom, margin = 22.dp)
+ start.linkTo(parent.start, margin = 22.dp)
+ end.linkTo(parent.end, margin = 22.dp)
+ width = Dimension.fillToConstraints
+ }
+ ) {
+ ActionButton(
+ text = stringResource(id = R.string.continue_login),
+ onClick = onContinueWithLogin,
+ isEnabled = state.hasTooManyDevices.not() && state.isLoading.not(),
+ colors = ButtonDefaults.buttonColors(
+ backgroundColor = colorResource(id = R.color.green),
+ disabledBackgroundColor = colorResource(id = R.color.green40),
+ disabledContentColor = colorResource(id = R.color.white80),
+ contentColor = Color.White
+ )
+ )
+ ActionButton(
+ text = stringResource(id = R.string.back),
+ onClick = onBackClick,
+ colors = ButtonDefaults.buttonColors(
+ backgroundColor = colorResource(id = R.color.blue),
+ contentColor = Color.White
+ ),
+ modifier = Modifier
+ .padding(top = 16.dp)
+ )
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceRevokedScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceRevokedScreen.kt
index fc58c2685f..ce92ef72aa 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceRevokedScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceRevokedScreen.kt
@@ -101,6 +101,7 @@ fun DeviceRevokedScreen(
text = stringResource(id = R.string.go_to_login),
onClick = { deviceRevokedViewModel.onGoToLoginClicked() },
colors = ButtonDefaults.buttonColors(
+ contentColor = Color.White,
backgroundColor = colorResource(
if (state == DeviceRevokedUiState.SECURED) {
R.color.red60
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeviceListUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeviceListUiState.kt
new file mode 100644
index 0000000000..9e048c2926
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeviceListUiState.kt
@@ -0,0 +1,19 @@
+package net.mullvad.mullvadvpn.compose.state
+
+import net.mullvad.mullvadvpn.model.Device
+
+data class DeviceListUiState(
+ val devices: List<Device>,
+ val isLoading: Boolean,
+ val deviceStagedForRemoval: Device?
+) {
+ val hasTooManyDevices = devices.count() >= 5
+
+ companion object {
+ val INITIAL = DeviceListUiState(
+ devices = listOf(),
+ isLoading = true,
+ deviceStagedForRemoval = null
+ )
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
index f86a8d69e2..8f939487e5 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
@@ -9,6 +9,7 @@ import net.mullvad.mullvadvpn.ipc.EventDispatcher
import net.mullvad.mullvadvpn.ui.serviceconnection.DeviceRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling
+import net.mullvad.mullvadvpn.viewmodel.DeviceListViewModel
import net.mullvad.mullvadvpn.viewmodel.DeviceRevokedViewModel
import net.mullvad.mullvadvpn.viewmodel.LoginViewModel
import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel
@@ -39,6 +40,7 @@ val uiModule = module {
single { DeviceRepository(get()) }
viewModel { LoginViewModel(get()) }
viewModel { DeviceRevokedViewModel(get()) }
+ viewModel { DeviceListViewModel(get()) }
}
const val APPS_SCOPE = "APPS_SCOPE"
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragments/DeviceListFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragments/DeviceListFragment.kt
new file mode 100644
index 0000000000..11ef615ad8
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragments/DeviceListFragment.kt
@@ -0,0 +1,76 @@
+package net.mullvad.mullvadvpn.ui.fragments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.res.colorResource
+import androidx.fragment.app.Fragment
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBar
+import net.mullvad.mullvadvpn.compose.screen.DeviceListScreen
+import net.mullvad.mullvadvpn.ui.LoginFragment
+import net.mullvad.mullvadvpn.ui.MainActivity
+import net.mullvad.mullvadvpn.viewmodel.DeviceListViewModel
+import org.koin.androidx.viewmodel.ext.android.viewModel
+
+class DeviceListFragment : Fragment() {
+
+ private val deviceListViewModel by viewModel<DeviceListViewModel>()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ deviceListViewModel.accountToken = arguments?.getString(ACCOUNT_TOKEN_ARGUMENT_KEY)
+
+ return inflater.inflate(R.layout.fragment_compose, container, false).apply {
+ findViewById<ComposeView>(R.id.compose_view).setContent {
+ val topColor = colorResource(R.color.blue)
+ ScaffoldWithTopBar(
+ topBarColor = topColor,
+ statusBarColor = topColor,
+ navigationBarColor = colorResource(id = R.color.darkBlue),
+ onSettingsClicked = this@DeviceListFragment::openSettings,
+ content = {
+ DeviceListScreen(
+ viewModel = deviceListViewModel,
+ onBackClick = this@DeviceListFragment::goBack,
+ onContinueWithLogin = this@DeviceListFragment::openLoginView
+ )
+ }
+ )
+ }
+ }
+ }
+
+ private fun openLoginView() {
+ val loginFragment = LoginFragment().apply {
+ if (deviceListViewModel.accountToken != null) {
+ arguments = Bundle().apply {
+ putString(
+ ACCOUNT_TOKEN_ARGUMENT_KEY,
+ deviceListViewModel.accountToken
+ )
+ }
+ }
+ }
+ parentFragmentManager.beginTransaction().apply {
+ replace(R.id.main_fragment, loginFragment)
+ addToBackStack(null)
+ commit()
+ }
+ }
+
+ private fun goBack() {
+ parentActivity()?.onBackPressed()
+ }
+
+ private fun parentActivity(): MainActivity? {
+ return (context as? MainActivity)
+ }
+
+ private fun openSettings() = parentActivity()?.openSettings()
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt
new file mode 100644
index 0000000000..027520d293
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt
@@ -0,0 +1,50 @@
+package net.mullvad.mullvadvpn.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import net.mullvad.mullvadvpn.compose.state.DeviceListUiState
+import net.mullvad.mullvadvpn.model.Device
+import net.mullvad.mullvadvpn.ui.serviceconnection.DeviceRepository
+import net.mullvad.mullvadvpn.util.safeLet
+
+class DeviceListViewModel(
+ private val deviceRepository: DeviceRepository
+) : ViewModel() {
+ private val _stagedForRemoval = MutableStateFlow<Device?>(null)
+ var accountToken: String? = null
+
+ val uiState = deviceRepository.deviceList
+ .combine(_stagedForRemoval) { deviceList, deviceStagedForRemoval ->
+ DeviceListUiState(
+ devices = deviceList,
+ isLoading = false,
+ deviceStagedForRemoval = deviceStagedForRemoval
+ )
+ }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), DeviceListUiState.INITIAL)
+
+ fun stageDeviceForRemoval(device: Device) {
+ _stagedForRemoval.value = device
+ }
+
+ fun clearStagedDevice() {
+ _stagedForRemoval.value = null
+ }
+
+ fun confirmRemoval() {
+ safeLet(accountToken, _stagedForRemoval.value) { token, device ->
+ deviceRepository.removeDevice(token, device.id)
+ _stagedForRemoval.value = null
+ }
+ }
+
+ fun refreshDeviceState() = deviceRepository.refreshDeviceState()
+
+ fun refreshDeviceList() = accountToken?.let { token ->
+ deviceRepository.refreshDeviceList(token)
+ }
+}
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
index 8f53508029..de78bd2b7a 100644
--- a/android/app/src/main/res/values/colors.xml
+++ b/android/app/src/main/res/values/colors.xml
@@ -15,6 +15,7 @@
<color name="green">#44AD4D</color>
<color name="green90">#E644AD4D</color>
<color name="green80">#CC44AD4D</color>
+ <color name="green40">#6644AD4D</color>
<color name="red">#FFE34039</color>
<color name="red95">#F2E34039</color>
<color name="red80">#CCE34039</color>