diff options
| author | saber safavi <saber.safavi@codic.se> | 2023-05-26 10:14:37 +0200 |
|---|---|---|
| committer | saber safavi <saber.safavi@codic.se> | 2023-07-13 14:49:50 +0200 |
| commit | aadc8490ddd84b9632e225ad6b68758579c81b70 (patch) | |
| tree | af50315734973badf44688cdb0c8869a40d5bef5 /android | |
| parent | 9dc4ac6e340474524cb7bc88164cdae951ee9c05 (diff) | |
| download | mullvadvpn-aadc8490ddd84b9632e225ad6b68758579c81b70.tar.xz mullvadvpn-aadc8490ddd84b9632e225ad6b68758579c81b70.zip | |
Add settings compose screen
Diffstat (limited to 'android')
6 files changed, 271 insertions, 279 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/ResourcesExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/ResourcesExtensions.kt new file mode 100644 index 0000000000..dca2a6aeb9 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/ResourcesExtensions.kt @@ -0,0 +1,44 @@ +package net.mullvad.mullvadvpn.compose.extensions + +import android.content.res.Resources +import net.mullvad.mullvadvpn.R +import org.joda.time.DateTime +import org.joda.time.Duration +import org.joda.time.PeriodType + +fun Resources.getExpiryQuantityString(accountExpiry: DateTime): String { + val remainingTime = Duration(DateTime.now(), accountExpiry) + + return getExpiryQuantityString(this, accountExpiry, remainingTime) +} + +private fun getExpiryQuantityString( + resources: Resources, + accountExpiry: DateTime, + remainingTime: Duration +): String { + if (remainingTime.isShorterThan(Duration.ZERO)) { + return resources.getString(R.string.out_of_time) + } else { + val remainingTimeInfo = + remainingTime.toPeriodTo(accountExpiry, PeriodType.yearMonthDayTime()) + + if (remainingTimeInfo.years > 0) { + return getRemainingText(resources, R.plurals.years_left, remainingTimeInfo.years) + } else if (remainingTimeInfo.months >= 3) { + return getRemainingText(resources, R.plurals.months_left, remainingTimeInfo.months) + } else if (remainingTimeInfo.months > 0 || remainingTimeInfo.days >= 1) { + return getRemainingText( + resources, + R.plurals.days_left, + remainingTime.standardDays.toInt() + ) + } else { + return resources.getString(R.string.less_than_a_day_left) + } + } +} + +private fun getRemainingText(resources: Resources, pluralId: Int, quantity: Int): String { + return resources.getQuantityString(pluralId, quantity, quantity) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt new file mode 100644 index 0000000000..34a2e560e5 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt @@ -0,0 +1,185 @@ +package net.mullvad.mullvadvpn.compose.screen + +import android.net.Uri +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import me.onebone.toolbar.ScrollStrategy +import me.onebone.toolbar.rememberCollapsingToolbarScaffoldState +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.cell.DefaultExternalLinkView +import net.mullvad.mullvadvpn.compose.cell.NavigationCellBody +import net.mullvad.mullvadvpn.compose.cell.NavigationComposeCell +import net.mullvad.mullvadvpn.compose.component.CollapsableAwareToolbarScaffold +import net.mullvad.mullvadvpn.compose.component.CollapsingTopBar +import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar +import net.mullvad.mullvadvpn.compose.extensions.itemWithDivider +import net.mullvad.mullvadvpn.compose.state.SettingsUiState +import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_TEST_TAG +import net.mullvad.mullvadvpn.compose.theme.Dimens +import net.mullvad.mullvadvpn.ui.extension.openLink + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun PreviewSettings() { + SettingsScreen( + uiState = + SettingsUiState(appVersion = "2222.22", isLoggedIn = true, isUpdateAvailable = true) + ) +} + +@ExperimentalMaterial3Api +@Composable +fun SettingsScreen( + uiState: SettingsUiState, + onVpnSettingCellClick: () -> Unit = {}, + onSplitTunnelingCellClick: () -> Unit = {}, + onReportProblemCellClick: () -> Unit = {}, + onBackClick: () -> Unit = {} +) { + val context = LocalContext.current + val lazyListState = rememberLazyListState() + val state = rememberCollapsingToolbarScaffoldState() + val progress = state.toolbarState.progress + + CollapsableAwareToolbarScaffold( + backgroundColor = MaterialTheme.colorScheme.background, + modifier = Modifier.fillMaxSize(), + state = state, + scrollStrategy = ScrollStrategy.ExitUntilCollapsed, + isEnabledWhenCollapsable = true, + toolbar = { + val scaffoldModifier = + Modifier.road( + whenCollapsed = Alignment.TopCenter, + whenExpanded = Alignment.BottomStart + ) + CollapsingTopBar( + backgroundColor = MaterialTheme.colorScheme.secondary, + onBackClicked = { onBackClick() }, + title = stringResource(id = R.string.settings), + progress = progress, + modifier = scaffoldModifier, + backTitle = String(), + shouldRotateBackButtonDown = true + ) + }, + ) { + LazyColumn( + modifier = + Modifier.drawVerticalScrollbar(lazyListState) + .testTag(LAZY_LIST_TEST_TAG) + .fillMaxWidth() + .wrapContentHeight() + .animateContentSize(), + state = lazyListState + ) { + item { Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding)) } + if (uiState.isLoggedIn) { + item { + NavigationComposeCell( + title = stringResource(id = R.string.settings_vpn), + onClick = { onVpnSettingCellClick() } + ) + } + + item { + Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing)) + NavigationComposeCell( + title = stringResource(id = R.string.split_tunneling), + onClick = { onSplitTunnelingCellClick() } + ) + Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing)) + } + } + item { + NavigationComposeCell( + title = stringResource(id = R.string.app_version), + onClick = { + context.openLink( + Uri.parse(context.resources.getString(R.string.download_url)) + ) + }, + bodyView = + @Composable { + NavigationCellBody( + content = uiState.appVersion, + contentBodyDescription = stringResource(id = R.string.app_version), + isExternalLink = true, + ) + }, + showWarning = uiState.isUpdateAvailable, + ) + } + if (uiState.isUpdateAvailable) { + item { + Text( + text = stringResource(id = R.string.update_available_footer), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSecondary, + modifier = + Modifier.background(MaterialTheme.colorScheme.secondary) + .padding( + start = Dimens.cellStartPadding, + top = Dimens.cellTopPadding, + end = Dimens.cellStartPadding, + bottom = Dimens.cellLabelVerticalPadding, + ) + ) + } + } + + itemWithDivider { + Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing)) + NavigationComposeCell( + title = stringResource(id = R.string.report_a_problem), + onClick = { onReportProblemCellClick() } + ) + } + + itemWithDivider { + val faqGuideLabel = stringResource(id = R.string.faqs_and_guides) + NavigationComposeCell( + title = faqGuideLabel, + bodyView = @Composable { DefaultExternalLinkView(faqGuideLabel) }, + onClick = { + context.openLink( + Uri.parse(context.resources.getString(R.string.faqs_and_guides_url)) + ) + } + ) + } + + itemWithDivider { + val privacyPolicyLabel = stringResource(id = R.string.privacy_policy_label) + NavigationComposeCell( + title = privacyPolicyLabel, + bodyView = @Composable { DefaultExternalLinkView(privacyPolicyLabel) }, + onClick = { + context.openLink( + Uri.parse(context.resources.getString(R.string.privacy_policy_url)) + ) + } + ) + } + } + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/SettingsFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/SettingsFragment.kt index 1aefb900fb..7758986702 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/SettingsFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/SettingsFragment.kt @@ -4,182 +4,66 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageButton -import androidx.core.content.ContextCompat -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.launch -import net.mullvad.mullvadvpn.BuildConfig +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.constant.BuildTypes -import net.mullvad.mullvadvpn.model.DeviceState -import net.mullvad.mullvadvpn.repository.DeviceRepository -import net.mullvad.mullvadvpn.ui.CollapsibleTitleController +import net.mullvad.mullvadvpn.compose.screen.SettingsScreen +import net.mullvad.mullvadvpn.compose.theme.AppTheme import net.mullvad.mullvadvpn.ui.NavigationBarPainter import net.mullvad.mullvadvpn.ui.StatusBarPainter -import net.mullvad.mullvadvpn.ui.VersionInfo -import net.mullvad.mullvadvpn.ui.paintNavigationBar -import net.mullvad.mullvadvpn.ui.paintStatusBar -import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager -import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState -import net.mullvad.mullvadvpn.ui.serviceconnection.appVersionInfoCache -import net.mullvad.mullvadvpn.ui.widget.AppVersionCell -import net.mullvad.mullvadvpn.ui.widget.NavigateCell -import net.mullvad.mullvadvpn.ui.widget.UrlCell -import net.mullvad.mullvadvpn.util.JobTracker -import net.mullvad.mullvadvpn.util.UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS -import net.mullvad.mullvadvpn.util.addDebounceForUnknownState -import net.mullvad.mullvadvpn.util.appVersionCallbackFlow -import org.koin.android.ext.android.inject +import net.mullvad.mullvadvpn.viewmodel.SettingsViewModel +import org.koin.androidx.viewmodel.ext.android.viewModel class SettingsFragment : BaseFragment(), StatusBarPainter, NavigationBarPainter { + private val vm by viewModel<SettingsViewModel>() - // Injected dependencies - private val deviceRepository: DeviceRepository by inject() - private val serviceConnectionManager: ServiceConnectionManager by inject() - - private lateinit var appVersionMenu: AppVersionCell - private lateinit var vpnSettingsMenu: View - private lateinit var splitTunnelingMenu: View - private lateinit var titleController: CollapsibleTitleController - - @Deprecated("Refactor code to instead rely on Lifecycle.") private val jobTracker = JobTracker() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - lifecycleScope.launchUiSubscriptionsOnResume() - } - + @OptIn(ExperimentalMaterial3Api::class) override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View { - val view = inflater.inflate(R.layout.settings, container, false) - - view.findViewById<ImageButton>(R.id.close).setOnClickListener { activity?.onBackPressed() } - - vpnSettingsMenu = - view.findViewById<NavigateCell>(R.id.vpn_settings).apply { - targetFragment = VpnSettingsFragment::class - } - - splitTunnelingMenu = - view.findViewById<NavigateCell>(R.id.split_tunneling).apply { - targetFragment = SplitTunnelingFragment::class - } - - view.findViewById<NavigateCell>(R.id.report_a_problem).apply { - targetFragment = ProblemReportFragment::class - } - - appVersionMenu = view.findViewById<AppVersionCell>(R.id.app_version) - - titleController = CollapsibleTitleController(view) - - view.findViewById<UrlCell>(R.id.faqs_and_guides).visibility = - if (BuildTypes.RELEASE == BuildConfig.BUILD_TYPE) { - View.GONE - } else { - View.VISIBLE - } - - return view - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - initializeUiState() - } - - override fun onResume() { - super.onResume() - paintNavigationBar(ContextCompat.getColor(requireContext(), R.color.darkBlue)) - } - - override fun onStop() { - jobTracker.cancelAllJobs() - super.onStop() - } - - override fun onDestroyView() { - titleController.onDestroy() - super.onDestroyView() - } - - private fun initializeUiState() { - updateLoggedInStatus(deviceRepository.deviceState.value is DeviceState.LoggedIn) - appVersionMenu.version = BuildConfig.VERSION_NAME - serviceConnectionManager.appVersionInfoCache().let { cache -> - updateVersionInfo( - if (cache != null) { - VersionInfo( - currentVersion = cache.version, - upgradeVersion = cache.upgradeVersion, - isOutdated = cache.isOutdated, - isSupported = cache.isSupported - ) - } else { - VersionInfo( - currentVersion = null, - upgradeVersion = null, - isOutdated = false, - isSupported = true + ): View? { + return inflater.inflate(R.layout.fragment_compose, container, false).apply { + findViewById<ComposeView>(R.id.compose_view).setContent { + AppTheme { + val state = vm.uiState.collectAsState().value + SettingsScreen( + uiState = state, + onVpnSettingCellClick = { openVpnSettingsFragment() }, + onSplitTunnelingCellClick = { openSplitTunnelingFragment() }, + onReportProblemCellClick = { openReportProblemFragment() }, + onBackClick = { activity?.onBackPressed() } ) } - ) - } - } - - private fun CoroutineScope.launchUiSubscriptionsOnResume() = launch { - repeatOnLifecycle(Lifecycle.State.RESUMED) { - launchPaintStatusBarAfterTransition() - luanchConfigureMenuOnDeviceChanges() - launchVersionInfoSubscription() + } } } - private fun CoroutineScope.launchPaintStatusBarAfterTransition() = launch { - transitionFinishedFlow.collect { - paintStatusBar(ContextCompat.getColor(requireContext(), R.color.darkBlue)) + private fun openFragment(fragment: Fragment) { + parentFragmentManager.beginTransaction().apply { + setCustomAnimations( + R.anim.fragment_enter_from_right, + R.anim.fragment_exit_to_left, + R.anim.fragment_half_enter_from_left, + R.anim.fragment_exit_to_right + ) + replace(R.id.main_fragment, fragment) + addToBackStack(null) + commitAllowingStateLoss() } } - private fun CoroutineScope.luanchConfigureMenuOnDeviceChanges() = launch { - deviceRepository.deviceState - .debounce { it.addDebounceForUnknownState(UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS) } - .collect { device -> updateLoggedInStatus(device is DeviceState.LoggedIn) } - } - - private fun CoroutineScope.launchVersionInfoSubscription() = launch { - serviceConnectionManager.connectionState - .flatMapLatest { state -> - if (state is ServiceConnectionState.ConnectedReady) { - state.container.appVersionInfoCache.appVersionCallbackFlow() - } else { - emptyFlow() - } - } - .collect { versionInfo -> updateVersionInfo(versionInfo) } + private fun openVpnSettingsFragment() { + openFragment(VpnSettingsFragment()) } - private fun updateLoggedInStatus(loggedIn: Boolean) { - val visibility = - if (loggedIn) { - View.VISIBLE - } else { - View.GONE - } - - vpnSettingsMenu.visibility = visibility - splitTunnelingMenu.visibility = visibility + private fun openSplitTunnelingFragment() { + openFragment(SplitTunnelingFragment()) } - private fun updateVersionInfo(versionInfo: VersionInfo) { - appVersionMenu.updateAvailable = versionInfo.isOutdated || !versionInfo.isSupported + private fun openReportProblemFragment() { + openFragment(ProblemReportFragment()) } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt index 38c8f8ed90..2e674ebc45 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt @@ -2,14 +2,12 @@ package net.mullvad.mullvadvpn.ui.notification import android.content.Context import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.util.TimeLeftFormatter +import net.mullvad.mullvadvpn.compose.extensions.getExpiryQuantityString import org.joda.time.DateTime class AccountExpiryNotification( - context: Context, + val context: Context, ) : InAppNotification() { - private val timeLeftFormatter = TimeLeftFormatter(context.resources) - init { status = StatusLevel.Error title = context.getString(R.string.account_credit_expires_soon) @@ -19,7 +17,7 @@ class AccountExpiryNotification( val threeDaysFromNow = DateTime.now().plusDays(3) if (expiry != null && expiry.isBefore(threeDaysFromNow)) { - message = timeLeftFormatter.format(expiry) + message = context.resources.getExpiryQuantityString(expiry) shouldShow = true } else { shouldShow = false diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/TimeLeftFormatter.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/TimeLeftFormatter.kt deleted file mode 100644 index c3a6aaa1cb..0000000000 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/TimeLeftFormatter.kt +++ /dev/null @@ -1,38 +0,0 @@ -package net.mullvad.mullvadvpn.util - -import android.content.res.Resources -import net.mullvad.mullvadvpn.R -import org.joda.time.DateTime -import org.joda.time.Duration -import org.joda.time.PeriodType - -class TimeLeftFormatter(val resources: Resources) { - fun format(accountExpiry: DateTime): String { - val remainingTime = Duration(DateTime.now(), accountExpiry) - - return format(accountExpiry, remainingTime) - } - - fun format(accountExpiry: DateTime, remainingTime: Duration): String { - if (remainingTime.isShorterThan(Duration.ZERO)) { - return resources.getString(R.string.out_of_time) - } else { - val remainingTimeInfo = - remainingTime.toPeriodTo(accountExpiry, PeriodType.yearMonthDayTime()) - - if (remainingTimeInfo.years > 0) { - return getRemainingText(R.plurals.years_left, remainingTimeInfo.years) - } else if (remainingTimeInfo.months >= 3) { - return getRemainingText(R.plurals.months_left, remainingTimeInfo.months) - } else if (remainingTimeInfo.months > 0 || remainingTimeInfo.days >= 1) { - return getRemainingText(R.plurals.days_left, remainingTime.standardDays.toInt()) - } else { - return resources.getString(R.string.less_than_a_day_left) - } - } - } - - private fun getRemainingText(pluralId: Int, quantity: Int): String { - return resources.getQuantityString(pluralId, quantity, quantity) - } -} diff --git a/android/app/src/main/res/layout/settings.xml b/android/app/src/main/res/layout/settings.xml deleted file mode 100644 index fe4d75ba99..0000000000 --- a/android/app/src/main/res/layout/settings.xml +++ /dev/null @@ -1,81 +0,0 @@ -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:mullvad="http://schemas.android.com/apk/res-auto" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@color/darkBlue" - android:gravity="left"> - <TextView android:id="@+id/title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/settings" - style="@style/SettingsCollapsedHeader" /> - <LinearLayout android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> - <FrameLayout android:layout_width="match_parent" - android:layout_height="wrap_content"> - <ImageButton android:id="@+id/close" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:padding="12dp" - android:background="?android:attr/selectableItemBackground" - android:src="@drawable/icon_close" /> - <TextView android:id="@+id/collapsed_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginHorizontal="4dp" - android:layout_gravity="center" - android:text="@string/settings" - style="@style/SettingsCollapsedHeader" /> - </FrameLayout> - <net.mullvad.mullvadvpn.ui.widget.ListenableScrollView android:id="@+id/scroll_area" - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <LinearLayout android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical"> - <TextView android:id="@+id/expanded_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="4dp" - android:layout_marginLeft="@dimen/side_margin" - android:lines="1" - android:text="@string/settings" - style="@style/SettingsExpandedHeader" /> - <net.mullvad.mullvadvpn.ui.widget.NavigateCell android:id="@+id/vpn_settings" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/vertical_space" - mullvad:text="@string/settings_vpn" /> - <net.mullvad.mullvadvpn.ui.widget.NavigateCell android:id="@+id/split_tunneling" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/vertical_space" - mullvad:text="@string/split_tunneling" /> - <net.mullvad.mullvadvpn.ui.widget.AppVersionCell android:id="@+id/app_version" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/vertical_space" - mullvad:text="@string/app_version" - mullvad:footer="@string/update_available_footer" /> - <net.mullvad.mullvadvpn.ui.widget.NavigateCell android:id="@+id/report_a_problem" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/vertical_space" - mullvad:text="@string/report_a_problem" /> - <net.mullvad.mullvadvpn.ui.widget.UrlCell android:id="@+id/faqs_and_guides" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="1dp" - mullvad:text="@string/faqs_and_guides" - mullvad:url="@string/faqs_and_guides_url" /> - <net.mullvad.mullvadvpn.ui.widget.UrlCell android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="1dp" - mullvad:text="@string/privacy_policy_label" - mullvad:url="@string/privacy_policy_url" /> - </LinearLayout> - </net.mullvad.mullvadvpn.ui.widget.ListenableScrollView> - </LinearLayout> -</FrameLayout> |
