From 94cfc25af94f85812770b2a704f8d0540401fe62 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Thu, 25 Jan 2024 17:03:28 -0500 Subject: [PATCH 01/26] Add utils class for Site Monitor feature --- .../ui/sitemonitor/SiteMonitorUtils.kt | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt new file mode 100644 index 000000000000..891931689048 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt @@ -0,0 +1,31 @@ +package org.wordpress.android.ui.sitemonitor + +import org.wordpress.android.WordPress +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.ui.WPWebViewActivity +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import javax.inject.Inject + +class SiteMonitorUtils @Inject constructor( + private val analyticsTrackerWrapper: AnalyticsTrackerWrapper +) { + fun getUserAgent() = WordPress.getUserAgent() + + fun getAuthenticationPostData(authenticationUrl: String, + urlToLoad: String, + username: String, + password: String, + token: String): String = + WPWebViewActivity.getAuthenticationPostData(authenticationUrl, urlToLoad, username, password, token) + + + fun trackActivityLaunched() { + analyticsTrackerWrapper.track(AnalyticsTracker.Stat.SITE_MONITORING_SCREEN_SHOWN) + } + + fun sanitizeSiteUrl(url: String?) = url?.replace(Regex(HTTP_PATTERN), "") ?: "" + + companion object { + const val HTTP_PATTERN = "(https?://)" + } +} From de18b624c763e04ba334083d9559f625fd301d1c Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Thu, 25 Jan 2024 17:04:23 -0500 Subject: [PATCH 02/26] Add the site monitor uiState and Model classes --- .../ui/sitemonitor/SiteMonitorUiState.kt | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUiState.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUiState.kt new file mode 100644 index 000000000000..42434621a8e3 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUiState.kt @@ -0,0 +1,67 @@ +package org.wordpress.android.ui.sitemonitor + +import org.wordpress.android.R +import org.wordpress.android.ui.utils.UiString + +sealed class SiteMonitorUiState { + object Preparing : SiteMonitorUiState() + + data class Prepared( + val model: SiteMonitorModel + ) : SiteMonitorUiState() + + object Loaded : SiteMonitorUiState() + + open class Error( + val title: UiString, + val description: UiString, + val button: ErrorButton? = null + ) : SiteMonitorUiState() { + data class ErrorButton( + val text: UiString, + val click: () -> Unit + ) + } + + data class NoNetworkError(val buttonClick: () -> Unit): Error( + title = UiString.UiStringRes(R.string.campaign_detail_no_network_error_title), + description = UiString.UiStringRes(R.string.campaign_detail_error_description), + button = ErrorButton( + text = UiString.UiStringRes(R.string.campaign_detail_error_button_text), + click = buttonClick + ) + ) + + data class GenericError(val buttonClick: () -> Unit): Error( + title = UiString.UiStringRes(R.string.campaign_detail_error_title), + description = UiString.UiStringRes(R.string.campaign_detail_error_description), + button = ErrorButton( + text = UiString.UiStringRes(R.string.campaign_detail_error_button_text), + click = buttonClick + ) + ) +} + +data class SiteMonitorModel( + val enableJavascript: Boolean = true, + val enableDomStorage: Boolean = true, + val enableChromeClient: Boolean = true, + val userAgent: String = "", + val urls: List = emptyList() +) { + fun getUrlByType(type: SiteMonitorUrl.SiteMonitorType): SiteMonitorUrl? { + return urls.find { it.type == type } + } +} + +data class SiteMonitorUrl( + val type: SiteMonitorType, + val url: String, + val addressToLoad: String +) { + enum class SiteMonitorType { + METRICS, + PHP_LOGS, + WEB_SERVER_LOGS + } +} From 604a1090069f37796d85149139221becfa958330 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Thu, 25 Jan 2024 17:05:06 -0500 Subject: [PATCH 03/26] Add the site monitor mapper helper to map to uiState --- .../ui/sitemonitor/SiteMonitorMapper.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorMapper.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorMapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorMapper.kt new file mode 100644 index 000000000000..ac21f85e5ba2 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorMapper.kt @@ -0,0 +1,21 @@ +package org.wordpress.android.ui.sitemonitor + +import javax.inject.Inject + +class SiteMonitorMapper @Inject constructor( + private val siteMonitorUtils: SiteMonitorUtils +) { + fun toPrepared(urls: List) = SiteMonitorUiState.Prepared( + model = SiteMonitorModel( + enableJavascript = true, + enableDomStorage = true, + userAgent = siteMonitorUtils.getUserAgent(), + enableChromeClient = true, + urls = urls + ) + ) + + fun toNoNetworkError(buttonClick: () -> Unit) = SiteMonitorUiState.NoNetworkError(buttonClick = buttonClick) + + fun toGenericError(buttonClick: () -> Unit) = SiteMonitorUiState.GenericError(buttonClick = buttonClick) +} From dea69ec82cc3d4f60d1cbefb3e66875345e93438 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Thu, 25 Jan 2024 17:10:06 -0500 Subject: [PATCH 04/26] Add the site monitor webview client --- .../ui/sitemonitor/SiteMonitorWebViewClient.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClient.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClient.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClient.kt new file mode 100644 index 000000000000..fd13c1309bcb --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClient.kt @@ -0,0 +1,17 @@ +package org.wordpress.android.ui.sitemonitor + +import android.webkit.WebResourceRequest +import android.webkit.WebView +import org.wordpress.android.util.ErrorManagedWebViewClient + +class SiteMonitorWebViewClient( + listener: SiteMonitorWebViewClientListener +) : ErrorManagedWebViewClient(listener) { + interface SiteMonitorWebViewClientListener : ErrorManagedWebViewClientListener { + fun onRedirectToExternalBrowser(url: String) + } + + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) : Boolean { + return false + } +} From aad1477192b4f03f5a5f3ee8d7701e23675f46c9 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Thu, 25 Jan 2024 17:10:50 -0500 Subject: [PATCH 05/26] Add the basics for building the uiState for Site Monitoring --- .../sitemonitor/SiteMonitorParentViewModel.kt | 127 +++++++++++++++++- 1 file changed, 121 insertions(+), 6 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt index 00c337746eed..60afa9910ff4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt @@ -1,11 +1,16 @@ package org.wordpress.android.ui.sitemonitor +import android.text.TextUtils +import android.util.Log import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher -import org.wordpress.android.analytics.AnalyticsTracker +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.AccountStore +import org.wordpress.android.fluxc.store.SiteStore import org.wordpress.android.modules.BG_THREAD -import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.viewmodel.ScopedViewModel import javax.inject.Inject import javax.inject.Named @@ -13,16 +18,126 @@ import javax.inject.Named @HiltViewModel class SiteMonitorParentViewModel @Inject constructor( @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, - private val analyticsTrackerWrapper: AnalyticsTrackerWrapper + private val networkUtilsWrapper: NetworkUtilsWrapper, + private val accountStore: AccountStore, + private val mapper: SiteMonitorMapper, + private val siteMonitorUtils: SiteMonitorUtils, + private val siteStore: SiteStore, ) : ScopedViewModel(bgDispatcher) { private lateinit var site: SiteModel + private val metricUrlTemplate = "https://wordpress.com/site-monitoring/{blog}" + private val phpLogsUrlTemplate = "https://wordpress.com/site-monitoring/{blog}/php" + private val webServerLogsUrlTemplate = "https://wordpress.com/site-monitoring/{blog}/web" + + private val _uiState = MutableStateFlow(SiteMonitorUiState.Preparing) + val uiState = _uiState as StateFlow + fun start(site: SiteModel) { this.site = site - trackActivityLaunched() + siteMonitorUtils.trackActivityLaunched() + + loadViews() + } + + private fun loadViews() { + Log.i(javaClass.simpleName, "***=> loadViews") + postUiState(SiteMonitorUiState.Preparing) + + if (!checkForInternetConnectivityAndPostErrorIfNeeded()) return + + if (!validateAndPostErrorIfNeeded()) return + + assembleAndShowSiteMonitor() + } + + private fun assembleAndShowSiteMonitor() { + val sanitizedUrl = siteMonitorUtils.sanitizeSiteUrl(site.url) + + val siteMonitorUrls = mutableListOf().apply { + add( + createSiteMonitorUrl( + metricUrlTemplate.replace("{blog}", sanitizedUrl), SiteMonitorUrl.SiteMonitorType.METRICS) + ) + add( + createSiteMonitorUrl( + phpLogsUrlTemplate.replace("{blog}", sanitizedUrl), + SiteMonitorUrl.SiteMonitorType.PHP_LOGS + ) + ) + add( + createSiteMonitorUrl( + webServerLogsUrlTemplate.replace("{blog}", sanitizedUrl), + SiteMonitorUrl.SiteMonitorType.WEB_SERVER_LOGS + ) + ) + } + postUiState(mapper.toPrepared(siteMonitorUrls)) + } + + private fun createSiteMonitorUrl(url: String, type: SiteMonitorUrl.SiteMonitorType): SiteMonitorUrl { + return SiteMonitorUrl( + type = type, + url = url, + addressToLoad = prepareAddressToLoad(url) + ) + } + private fun prepareAddressToLoad(url: String): String { + val username = accountStore.account.userName + val accessToken = accountStore.accessToken + + var addressToLoad = url + + // Custom domains are not properly authenticated due to a server side(?) issue, so this gets around that + if (!addressToLoad.contains(WPCOM_DOMAIN)) { + val wpComSites: List = siteStore.wPComSites + for (siteModel in wpComSites) { + // Only replace the url if we know the unmapped url and if it's a custom domain + if (!TextUtils.isEmpty(siteModel.unmappedUrl) + && !siteModel.url.contains(WPCOM_DOMAIN) + ) { + addressToLoad = addressToLoad.replace(siteModel.url, siteModel.unmappedUrl) + } + } + } + return siteMonitorUtils.getAuthenticationPostData( + WPCOM_LOGIN_URL, + addressToLoad, + username, + "", + accessToken?:"" + ) + } + private fun checkForInternetConnectivityAndPostErrorIfNeeded(): Boolean { + if (networkUtilsWrapper.isNetworkAvailable()) return true + postUiState(mapper.toNoNetworkError(this@SiteMonitorParentViewModel::loadViews)) + return false + } + + private fun validateAndPostErrorIfNeeded(): Boolean { + if (accountStore.account.userName.isNullOrEmpty() || accountStore.accessToken.isNullOrEmpty()) { + postUiState(mapper.toGenericError(this@SiteMonitorParentViewModel::loadViews)) + return false + } + return true + } + + private fun postUiState(uiState: SiteMonitorUiState) { + launch { + _uiState.value = uiState + } + } + + fun onUrlLoaded() { + postUiState(SiteMonitorUiState.Loaded) + } + + fun onWebViewError() { + postUiState(mapper.toGenericError(this@SiteMonitorParentViewModel::loadViews)) } - private fun trackActivityLaunched() { - analyticsTrackerWrapper.track(AnalyticsTracker.Stat.SITE_MONITORING_SCREEN_SHOWN) + companion object { + const val WPCOM_LOGIN_URL = "https://wordpress.com/wp-login.php" + const val WPCOM_DOMAIN = ".wordpress.com" } } From ffc8b5998ce3e34ccd8e4bd80c820f751a7499eb Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Thu, 25 Jan 2024 17:11:55 -0500 Subject: [PATCH 06/26] Refactor to support showing web views in each tab for metrics, php logs, and web server logs --- .../sitemonitor/SiteMonitorParentActivity.kt | 129 ++++++++++++++++-- 1 file changed, 118 insertions(+), 11 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt index 96087a86b48e..6e7b3df2048e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt @@ -2,35 +2,63 @@ package org.wordpress.android.ui.sitemonitor import android.annotation.SuppressLint import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.webkit.WebView import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +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.TabRow import androidx.compose.material.Text import androidx.compose.material3.Tab import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf 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.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.WPWebViewActivity import org.wordpress.android.ui.compose.components.MainTopAppBar import org.wordpress.android.ui.compose.components.NavigationIcons import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.compose.utils.uiStringText +import org.wordpress.android.ui.main.jetpack.migration.compose.state.LoadingState +import org.wordpress.android.ui.sitemonitor.SiteMonitorWebViewClient.SiteMonitorWebViewClientListener import org.wordpress.android.util.extensions.getSerializableExtraCompat @AndroidEntryPoint -class SiteMonitorParentActivity: AppCompatActivity() { - val viewModel:SiteMonitorParentViewModel by viewModels() +class SiteMonitorParentActivity: AppCompatActivity(), SiteMonitorWebViewClientListener { + override fun onRedirectToExternalBrowser(url: String) { + // todo: not sure if this is needed + } + + override fun onWebViewPageLoaded() = viewModel.onUrlLoaded() + + override fun onWebViewReceivedError() = viewModel.onWebViewError() + +val viewModel:SiteMonitorParentViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -48,7 +76,9 @@ class SiteMonitorParentActivity: AppCompatActivity() { @Composable @SuppressLint("UnusedMaterialScaffoldPaddingParameter") - fun SiteMonitorScreen(modifier: Modifier = Modifier) { + fun SiteMonitorScreen(modifier: Modifier = Modifier, + viewModel: SiteMonitorParentViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) { + val uiState by viewModel.uiState.collectAsState() Scaffold( topBar = { MainTopAppBar( @@ -58,15 +88,15 @@ class SiteMonitorParentActivity: AppCompatActivity() { ) }, content = { - TabScreen(modifier = modifier) + TabScreen(modifier = modifier, uiState) } ) } @Composable @SuppressLint("UnusedMaterialScaffoldPaddingParameter") - fun TabScreen(modifier: Modifier = Modifier) { - var tabIndex by remember { mutableStateOf(0) } + fun TabScreen(modifier: Modifier = Modifier, uiState: SiteMonitorUiState) { + var tabIndex by remember { mutableIntStateOf(0) } val tabs = listOf( R.string.site_monitoring_tab_title_metrics, @@ -88,15 +118,92 @@ class SiteMonitorParentActivity: AppCompatActivity() { } } when (tabIndex) { - 0 -> SiteMonitoringWebView() - 1 -> SiteMonitoringWebView() - 2 -> SiteMonitoringWebView() + 0 -> SiteMonitoringWebViewForTab(uiState, SiteMonitorUrl.SiteMonitorType.METRICS) + 1 -> SiteMonitoringWebViewForTab(uiState, SiteMonitorUrl.SiteMonitorType.PHP_LOGS) + 2 -> SiteMonitoringWebViewForTab(uiState, SiteMonitorUrl.SiteMonitorType.WEB_SERVER_LOGS) } } } @Composable - fun SiteMonitoringWebView(){ - Text(text = "SiteMonitoringWebView") + fun SiteMonitoringWebViewForTab(uiState: SiteMonitorUiState, tab: SiteMonitorUrl.SiteMonitorType) { + when (uiState) { + is SiteMonitorUiState.Preparing -> LoadingState() + is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded -> SiteMonitoringWebView(uiState, tab) + is SiteMonitorUiState.Error -> SiteMonitorError(uiState) + } + } + + @Composable + fun SiteMonitorError(error: SiteMonitorUiState.Error) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .padding(20.dp) + .fillMaxWidth() + .fillMaxHeight(), + ) { + Text( + text = uiStringText(uiString = error.title), + style = MaterialTheme.typography.h5, + textAlign = TextAlign.Center + ) + Text( + text = uiStringText(uiString = error.description), + style = MaterialTheme.typography.body1, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 8.dp) + ) + if (error.button != null) { + Button( + modifier = Modifier.padding(top = 8.dp), + onClick = error.button.click + ) { + Text(text = uiStringText(uiString = error.button.text)) + } + } + } + } + @SuppressLint("SetJavaScriptEnabled") + @Composable + fun SiteMonitoringWebView(uiState: SiteMonitorUiState, tab: SiteMonitorUrl.SiteMonitorType) { + var webView: WebView? by remember { mutableStateOf(null) } + + if (uiState is SiteMonitorUiState.Prepared) { + val model = uiState.model + LaunchedEffect(true) { + webView = WebView(this@SiteMonitorParentActivity).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY + settings.userAgentString = model.userAgent + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + webViewClient = SiteMonitorWebViewClient(this@SiteMonitorParentActivity) + model.getUrlByType(tab)?.addressToLoad?.let { + postUrl(WPWebViewActivity.WPCOM_LOGIN_URL, it.toByteArray()) + } + } + } + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + if (uiState is SiteMonitorUiState.Prepared) { + LoadingState() + } else { + webView?.let { theWebView -> + AndroidView( + factory = { theWebView }, + modifier = Modifier.fillMaxSize() + ) + } + } + } } } From 7dd8cac9b175f3cd3461818485fb7071c8191cff Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Thu, 25 Jan 2024 17:12:32 -0500 Subject: [PATCH 07/26] Refactor: remove log lines --- .../android/ui/sitemonitor/SiteMonitorParentViewModel.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt index 60afa9910ff4..38e2b43fc1fd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt @@ -1,7 +1,6 @@ package org.wordpress.android.ui.sitemonitor import android.text.TextUtils -import android.util.Log import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow @@ -41,7 +40,6 @@ class SiteMonitorParentViewModel @Inject constructor( } private fun loadViews() { - Log.i(javaClass.simpleName, "***=> loadViews") postUiState(SiteMonitorUiState.Preparing) if (!checkForInternetConnectivityAndPostErrorIfNeeded()) return From 8845481641fc6beffcb69e747f20946968eced97 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Fri, 26 Jan 2024 11:40:46 -0500 Subject: [PATCH 08/26] Refactor: remove SiteMonitorURL and list. Replace with url and addressToLoad --- .../ui/sitemonitor/SiteMonitorUiState.kt | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUiState.kt index 42434621a8e3..50bca3c0dec8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUiState.kt @@ -43,25 +43,16 @@ sealed class SiteMonitorUiState { } data class SiteMonitorModel( + val siteMonitorType: SiteMonitorType, val enableJavascript: Boolean = true, val enableDomStorage: Boolean = true, val enableChromeClient: Boolean = true, val userAgent: String = "", - val urls: List = emptyList() -) { - fun getUrlByType(type: SiteMonitorUrl.SiteMonitorType): SiteMonitorUrl? { - return urls.find { it.type == type } - } -} - -data class SiteMonitorUrl( - val type: SiteMonitorType, val url: String, val addressToLoad: String -) { - enum class SiteMonitorType { - METRICS, - PHP_LOGS, - WEB_SERVER_LOGS - } +) +enum class SiteMonitorType { + METRICS, + PHP_LOGS, + WEB_SERVER_LOGS } From a57a6d81da1c80d5f8dbc8e087dc70e2edd381f9 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Fri, 26 Jan 2024 11:41:26 -0500 Subject: [PATCH 09/26] Refactor: remove SiteMonitorURL list. Replace with values specific to that type --- .../wordpress/android/ui/sitemonitor/SiteMonitorMapper.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorMapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorMapper.kt index ac21f85e5ba2..f4bcb0d3c619 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorMapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorMapper.kt @@ -5,13 +5,15 @@ import javax.inject.Inject class SiteMonitorMapper @Inject constructor( private val siteMonitorUtils: SiteMonitorUtils ) { - fun toPrepared(urls: List) = SiteMonitorUiState.Prepared( + fun toPrepared(url: String, addressToLoad: String, siteMonitorType: SiteMonitorType) = SiteMonitorUiState.Prepared( model = SiteMonitorModel( enableJavascript = true, enableDomStorage = true, userAgent = siteMonitorUtils.getUserAgent(), enableChromeClient = true, - urls = urls + url = url, + addressToLoad = addressToLoad, + siteMonitorType = siteMonitorType ) ) From 5a2f30f6a3453b895b2d9952e4d174c19495aa0b Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Fri, 26 Jan 2024 11:42:03 -0500 Subject: [PATCH 10/26] Refactor: create a more specific web client so the URLs can be passed along to the activity/vm --- .../sitemonitor/SiteMonitorWebViewClient.kt | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClient.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClient.kt index fd13c1309bcb..29305e0d2ea0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClient.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClient.kt @@ -1,17 +1,46 @@ package org.wordpress.android.ui.sitemonitor +import android.graphics.Bitmap +import android.webkit.WebResourceError import android.webkit.WebResourceRequest import android.webkit.WebView -import org.wordpress.android.util.ErrorManagedWebViewClient +import android.webkit.WebViewClient class SiteMonitorWebViewClient( - listener: SiteMonitorWebViewClientListener -) : ErrorManagedWebViewClient(listener) { - interface SiteMonitorWebViewClientListener : ErrorManagedWebViewClientListener { - fun onRedirectToExternalBrowser(url: String) + private val listener: SiteMonitorWebViewClientListener +) : WebViewClient() { + private var errorReceived = false + private var requestedUrl: String? = null + interface SiteMonitorWebViewClientListener { + fun onWebViewPageLoaded(url: String) + fun onWebViewReceivedError(url: String) } - - override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) : Boolean { + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { return false } + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + errorReceived = false + requestedUrl = url + } + + override fun onPageFinished(view: WebView, url: String?) { + super.onPageFinished(view, url) + if (!errorReceived) { + url?.let { listener.onWebViewPageLoaded(it) } + } + } + + override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) { + super.onReceivedError(view, request, error) + // From the documentation: + // > These errors usually indicate inability to connect to the server. + // > will be called for any resource (iframe, image, etc.), not just for the main page. + // > Thus, it is recommended to perform minimum required work in this callback. + if (request?.isForMainFrame == true && requestedUrl == request.url.toString()) { + errorReceived = true + listener.onWebViewReceivedError(request.url.toString()) + } + } } From 5aba4a3666a071b40e335097466fba3710ed1d61 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Fri, 26 Jan 2024 11:42:22 -0500 Subject: [PATCH 11/26] Add a helper function to convert from url to siteMonitor type --- .../android/ui/sitemonitor/SiteMonitorUtils.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt index 891931689048..dbaa7b25cebd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt @@ -25,7 +25,17 @@ class SiteMonitorUtils @Inject constructor( fun sanitizeSiteUrl(url: String?) = url?.replace(Regex(HTTP_PATTERN), "") ?: "" + fun urlToType(url: String): SiteMonitorType { + return when { + url.contains(PHP_LOGS_PATTERN) -> SiteMonitorType.PHP_LOGS + url.contains(WEB_SERVER_LOGS_PATTERN) -> SiteMonitorType.WEB_SERVER_LOGS + else -> SiteMonitorType.METRICS + } + } + companion object { const val HTTP_PATTERN = "(https?://)" + const val PHP_LOGS_PATTERN = "/php" + const val WEB_SERVER_LOGS_PATTERN = "/web" } } From a7e48c5766dba115c645fc91f38fdaf181816709 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Fri, 26 Jan 2024 11:42:59 -0500 Subject: [PATCH 12/26] Refactor: Use a Map for the uiStates instead of a single uiState, so that each tab can hold it's own state --- .../sitemonitor/SiteMonitorParentViewModel.kt | 83 +++++++++---------- 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt index 38e2b43fc1fd..b691949c07a0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt @@ -29,8 +29,9 @@ class SiteMonitorParentViewModel @Inject constructor( private val phpLogsUrlTemplate = "https://wordpress.com/site-monitoring/{blog}/php" private val webServerLogsUrlTemplate = "https://wordpress.com/site-monitoring/{blog}/web" - private val _uiState = MutableStateFlow(SiteMonitorUiState.Preparing) - val uiState = _uiState as StateFlow + private val _uiStates = MutableStateFlow>(emptyMap()) + val uiStates: StateFlow> = _uiStates + fun start(site: SiteModel) { this.site = site @@ -40,46 +41,30 @@ class SiteMonitorParentViewModel @Inject constructor( } private fun loadViews() { - postUiState(SiteMonitorUiState.Preparing) + SiteMonitorType.entries.forEach { type -> + postUiState(type, SiteMonitorUiState.Preparing) - if (!checkForInternetConnectivityAndPostErrorIfNeeded()) return + if (!checkForInternetConnectivityAndPostErrorIfNeeded(type)) return@forEach - if (!validateAndPostErrorIfNeeded()) return + if (!validateAndPostErrorIfNeeded(type)) return@forEach - assembleAndShowSiteMonitor() + assembleAndShowSiteMonitor(type) + } } - private fun assembleAndShowSiteMonitor() { + private fun assembleAndShowSiteMonitor(type: SiteMonitorType) { val sanitizedUrl = siteMonitorUtils.sanitizeSiteUrl(site.url) - - val siteMonitorUrls = mutableListOf().apply { - add( - createSiteMonitorUrl( - metricUrlTemplate.replace("{blog}", sanitizedUrl), SiteMonitorUrl.SiteMonitorType.METRICS) - ) - add( - createSiteMonitorUrl( - phpLogsUrlTemplate.replace("{blog}", sanitizedUrl), - SiteMonitorUrl.SiteMonitorType.PHP_LOGS - ) - ) - add( - createSiteMonitorUrl( - webServerLogsUrlTemplate.replace("{blog}", sanitizedUrl), - SiteMonitorUrl.SiteMonitorType.WEB_SERVER_LOGS - ) - ) - } - postUiState(mapper.toPrepared(siteMonitorUrls)) + val url = when (type) { + SiteMonitorType.METRICS -> metricUrlTemplate + SiteMonitorType.PHP_LOGS -> phpLogsUrlTemplate + SiteMonitorType.WEB_SERVER_LOGS -> webServerLogsUrlTemplate + }.replace("{blog}", sanitizedUrl) + + val addressToLoad = prepareAddressToLoad(url) + val uiState = mapper.toPrepared(url, addressToLoad, type) + postUiState(type, uiState) } - private fun createSiteMonitorUrl(url: String, type: SiteMonitorUrl.SiteMonitorType): SiteMonitorUrl { - return SiteMonitorUrl( - type = type, - url = url, - addressToLoad = prepareAddressToLoad(url) - ) - } private fun prepareAddressToLoad(url: String): String { val username = accountStore.account.userName val accessToken = accountStore.accessToken @@ -106,32 +91,40 @@ class SiteMonitorParentViewModel @Inject constructor( accessToken?:"" ) } - private fun checkForInternetConnectivityAndPostErrorIfNeeded(): Boolean { + private fun checkForInternetConnectivityAndPostErrorIfNeeded(type: SiteMonitorType) : Boolean { if (networkUtilsWrapper.isNetworkAvailable()) return true - postUiState(mapper.toNoNetworkError(this@SiteMonitorParentViewModel::loadViews)) - return false + postUiState(type, mapper.toNoNetworkError(this@SiteMonitorParentViewModel::loadViews)) + return false } - private fun validateAndPostErrorIfNeeded(): Boolean { + private fun validateAndPostErrorIfNeeded(type: SiteMonitorType): Boolean { if (accountStore.account.userName.isNullOrEmpty() || accountStore.accessToken.isNullOrEmpty()) { - postUiState(mapper.toGenericError(this@SiteMonitorParentViewModel::loadViews)) + postUiState(type, mapper.toGenericError(this@SiteMonitorParentViewModel::loadViews)) return false } return true } - private fun postUiState(uiState: SiteMonitorUiState) { + private fun postUiState(type: SiteMonitorType, uiState: SiteMonitorUiState) { launch { - _uiState.value = uiState + _uiStates.value = _uiStates.value.toMutableMap().apply { + this[type] = uiState + } } } - fun onUrlLoaded() { - postUiState(SiteMonitorUiState.Loaded) + fun onUrlLoaded(url: String) { + val currentState = _uiStates.value + val type = siteMonitorUtils.urlToType(url) + val updatedState = currentState + (type to SiteMonitorUiState.Loaded) + _uiStates.value = updatedState } - fun onWebViewError() { - postUiState(mapper.toGenericError(this@SiteMonitorParentViewModel::loadViews)) + fun onWebViewError(url: String) { + val currentState = _uiStates.value + val type = siteMonitorUtils.urlToType(url) + val updatedState = currentState + (type to mapper.toGenericError(this@SiteMonitorParentViewModel::loadViews)) + _uiStates.value = updatedState } companion object { From 8049c13ff94b5cd900c37919d58ea17660160924 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Fri, 26 Jan 2024 11:43:43 -0500 Subject: [PATCH 13/26] Refactor: Handle a Map of uiStates instead of a single uiState. Use type to determine views. This needs more work --- .../sitemonitor/SiteMonitorParentActivity.kt | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt index 6e7b3df2048e..3c1537d45533 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt @@ -50,13 +50,9 @@ import org.wordpress.android.util.extensions.getSerializableExtraCompat @AndroidEntryPoint class SiteMonitorParentActivity: AppCompatActivity(), SiteMonitorWebViewClientListener { - override fun onRedirectToExternalBrowser(url: String) { - // todo: not sure if this is needed - } - - override fun onWebViewPageLoaded() = viewModel.onUrlLoaded() + override fun onWebViewPageLoaded(url: String) = viewModel.onUrlLoaded(url) - override fun onWebViewReceivedError() = viewModel.onWebViewError() + override fun onWebViewReceivedError(url: String) = viewModel.onWebViewError(url) val viewModel:SiteMonitorParentViewModel by viewModels() @@ -78,7 +74,7 @@ val viewModel:SiteMonitorParentViewModel by viewModels() @SuppressLint("UnusedMaterialScaffoldPaddingParameter") fun SiteMonitorScreen(modifier: Modifier = Modifier, viewModel: SiteMonitorParentViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) { - val uiState by viewModel.uiState.collectAsState() + val uiStates by viewModel.uiStates.collectAsState() Scaffold( topBar = { MainTopAppBar( @@ -88,14 +84,14 @@ val viewModel:SiteMonitorParentViewModel by viewModels() ) }, content = { - TabScreen(modifier = modifier, uiState) + TabScreen(modifier = modifier, uiStates) } ) } @Composable @SuppressLint("UnusedMaterialScaffoldPaddingParameter") - fun TabScreen(modifier: Modifier = Modifier, uiState: SiteMonitorUiState) { + fun TabScreen(modifier: Modifier = Modifier, uiStates: Map) { var tabIndex by remember { mutableIntStateOf(0) } val tabs = listOf( @@ -104,6 +100,12 @@ val viewModel:SiteMonitorParentViewModel by viewModels() R.string.site_monitoring_tab_title_web_server_logs ) + val tabsToType = mapOf( + 0 to SiteMonitorType.METRICS, + 1 to SiteMonitorType.PHP_LOGS, + 2 to SiteMonitorType.WEB_SERVER_LOGS + ) + Column(modifier = modifier.fillMaxWidth()) { TabRow( selectedTabIndex = tabIndex, @@ -117,19 +119,16 @@ val viewModel:SiteMonitorParentViewModel by viewModels() ) } } - when (tabIndex) { - 0 -> SiteMonitoringWebViewForTab(uiState, SiteMonitorUrl.SiteMonitorType.METRICS) - 1 -> SiteMonitoringWebViewForTab(uiState, SiteMonitorUrl.SiteMonitorType.PHP_LOGS) - 2 -> SiteMonitoringWebViewForTab(uiState, SiteMonitorUrl.SiteMonitorType.WEB_SERVER_LOGS) - } + val uiState = uiStates[tabsToType[tabIndex]] as SiteMonitorUiState + SiteMonitoringWebViewForTab(uiState) } } @Composable - fun SiteMonitoringWebViewForTab(uiState: SiteMonitorUiState, tab: SiteMonitorUrl.SiteMonitorType) { + fun SiteMonitoringWebViewForTab(uiState: SiteMonitorUiState) { when (uiState) { is SiteMonitorUiState.Preparing -> LoadingState() - is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded -> SiteMonitoringWebView(uiState, tab) + is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded -> SiteMonitoringWebView(uiState) is SiteMonitorUiState.Error -> SiteMonitorError(uiState) } } @@ -167,7 +166,7 @@ val viewModel:SiteMonitorParentViewModel by viewModels() } @SuppressLint("SetJavaScriptEnabled") @Composable - fun SiteMonitoringWebView(uiState: SiteMonitorUiState, tab: SiteMonitorUrl.SiteMonitorType) { + fun SiteMonitoringWebView(uiState: SiteMonitorUiState) { var webView: WebView? by remember { mutableStateOf(null) } if (uiState is SiteMonitorUiState.Prepared) { @@ -183,9 +182,7 @@ val viewModel:SiteMonitorParentViewModel by viewModels() settings.javaScriptEnabled = true settings.domStorageEnabled = true webViewClient = SiteMonitorWebViewClient(this@SiteMonitorParentActivity) - model.getUrlByType(tab)?.addressToLoad?.let { - postUrl(WPWebViewActivity.WPCOM_LOGIN_URL, it.toByteArray()) - } + postUrl(WPWebViewActivity.WPCOM_LOGIN_URL, model.addressToLoad.toByteArray()) } } } From c21a0de61a37a69b87943c4767d02b43f09c2fcb Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sat, 27 Jan 2024 19:17:02 -0500 Subject: [PATCH 14/26] Refactor: Update the webClient methods, add onTabSelected and adjust loadViews to handle a single or all types --- .../sitemonitor/SiteMonitorParentViewModel.kt | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt index b691949c07a0..ef09fe2a216d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt @@ -40,16 +40,23 @@ class SiteMonitorParentViewModel @Inject constructor( loadViews() } - private fun loadViews() { - SiteMonitorType.entries.forEach { type -> - postUiState(type, SiteMonitorUiState.Preparing) + private fun loadViews(siteMonitorType: SiteMonitorType? = null) { + if (siteMonitorType != null) { + loadIndividualView(siteMonitorType) + return + } + + SiteMonitorType.entries.forEach { type -> loadIndividualView(type) } + } - if (!checkForInternetConnectivityAndPostErrorIfNeeded(type)) return@forEach + private fun loadIndividualView(siteMonitorType: SiteMonitorType) { + postUiState(siteMonitorType, SiteMonitorUiState.Preparing) - if (!validateAndPostErrorIfNeeded(type)) return@forEach + if (!checkForInternetConnectivityAndPostErrorIfNeeded(siteMonitorType)) return - assembleAndShowSiteMonitor(type) - } + if (!validateAndPostErrorIfNeeded(siteMonitorType)) return + + assembleAndShowSiteMonitor(siteMonitorType) } private fun assembleAndShowSiteMonitor(type: SiteMonitorType) { @@ -94,7 +101,7 @@ class SiteMonitorParentViewModel @Inject constructor( private fun checkForInternetConnectivityAndPostErrorIfNeeded(type: SiteMonitorType) : Boolean { if (networkUtilsWrapper.isNetworkAvailable()) return true postUiState(type, mapper.toNoNetworkError(this@SiteMonitorParentViewModel::loadViews)) - return false + return false } private fun validateAndPostErrorIfNeeded(type: SiteMonitorType): Boolean { @@ -114,17 +121,17 @@ class SiteMonitorParentViewModel @Inject constructor( } fun onUrlLoaded(url: String) { - val currentState = _uiStates.value val type = siteMonitorUtils.urlToType(url) - val updatedState = currentState + (type to SiteMonitorUiState.Loaded) - _uiStates.value = updatedState + postUiState(type, SiteMonitorUiState.Loaded) } fun onWebViewError(url: String) { - val currentState = _uiStates.value val type = siteMonitorUtils.urlToType(url) - val updatedState = currentState + (type to mapper.toGenericError(this@SiteMonitorParentViewModel::loadViews)) - _uiStates.value = updatedState + postUiState(type, mapper.toGenericError(this@SiteMonitorParentViewModel::loadViews)) + } + + fun onTabSelected(siteMonitorType: SiteMonitorType?) { + loadViews(siteMonitorType) } companion object { From 09d2aa4fd6508f88c85b9e81702820fd62d203e7 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sat, 27 Jan 2024 19:18:33 -0500 Subject: [PATCH 15/26] [WIP] Separate WebView composables by type, send onTabSelected back to webview for a state update. --- .../sitemonitor/SiteMonitorParentActivity.kt | 153 ++++++++++++++++-- 1 file changed, 142 insertions(+), 11 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt index 3c1537d45533..3bae47007aa0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt @@ -15,7 +15,10 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.Button +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.TabRow @@ -44,7 +47,6 @@ import org.wordpress.android.ui.compose.components.MainTopAppBar import org.wordpress.android.ui.compose.components.NavigationIcons import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.utils.uiStringText -import org.wordpress.android.ui.main.jetpack.migration.compose.state.LoadingState import org.wordpress.android.ui.sitemonitor.SiteMonitorWebViewClient.SiteMonitorWebViewClientListener import org.wordpress.android.util.extensions.getSerializableExtraCompat @@ -54,7 +56,7 @@ class SiteMonitorParentActivity: AppCompatActivity(), SiteMonitorWebViewClientLi override fun onWebViewReceivedError(url: String) = viewModel.onWebViewError(url) -val viewModel:SiteMonitorParentViewModel by viewModels() + val viewModel:SiteMonitorParentViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -115,21 +117,73 @@ val viewModel:SiteMonitorParentViewModel by viewModels() tabs.forEachIndexed { index, title -> Tab(text = { Text(stringResource(id = title)) }, selected = tabIndex == index, - onClick = { tabIndex = index } + onClick = { + tabIndex = index + viewModel.onTabSelected(tabsToType[index]) + } ) } } - val uiState = uiStates[tabsToType[tabIndex]] as SiteMonitorUiState - SiteMonitoringWebViewForTab(uiState) + val siteMonitorType = tabsToType[tabIndex] ?: SiteMonitorType.METRICS + val uiState = uiStates[siteMonitorType] as SiteMonitorUiState + when(siteMonitorType) { + SiteMonitorType.METRICS -> SiteMonitoringWebViewForMetric(uiState) + SiteMonitorType.PHP_LOGS -> SiteMonitoringWebViewForTabPhp(uiState) + SiteMonitorType.WEB_SERVER_LOGS -> SiteMonitoringWebViewForTabWeb(uiState) + } } } @Composable - fun SiteMonitoringWebViewForTab(uiState: SiteMonitorUiState) { - when (uiState) { - is SiteMonitorUiState.Preparing -> LoadingState() - is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded -> SiteMonitoringWebView(uiState) - is SiteMonitorUiState.Error -> SiteMonitorError(uiState) + fun SiteMonitoringWebViewForMetric(uiState: SiteMonitorUiState) { + LazyColumn { + item { + when (uiState) { + is SiteMonitorUiState.Preparing -> LoadingState() + is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded -> SiteMonitoringWebViewMetric(uiState) + is SiteMonitorUiState.Error -> SiteMonitorError(uiState) + } + } + } + } + + @SuppressLint("SetJavaScriptEnabled") + @Composable + fun SiteMonitoringWebViewMetric(uiState: SiteMonitorUiState) { + var webView: WebView? by remember { mutableStateOf(null) } + + if (uiState is SiteMonitorUiState.Prepared) { + val model = uiState.model + LaunchedEffect(true) { + webView = WebView(this@SiteMonitorParentActivity).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY + settings.userAgentString = model.userAgent + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + webViewClient = SiteMonitorWebViewClient(this@SiteMonitorParentActivity) + postUrl(WPWebViewActivity.WPCOM_LOGIN_URL, model.addressToLoad.toByteArray()) + } + } + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + if (uiState is SiteMonitorUiState.Prepared) { + LoadingState() + } else { + webView?.let { theWebView -> + AndroidView( + factory = { theWebView }, + modifier = Modifier.fillMaxSize() + ) + } + } } } @@ -164,9 +218,73 @@ val viewModel:SiteMonitorParentViewModel by viewModels() } } } + @Composable + fun SiteMonitoringWebViewForTabPhp(uiState: SiteMonitorUiState) { + LazyColumn { + item { + when (uiState) { + is SiteMonitorUiState.Preparing -> LoadingState() + is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded -> SiteMonitoringWebViewPhp(uiState) + is SiteMonitorUiState.Error -> SiteMonitorError(uiState) + } + } + } + } + @SuppressLint("SetJavaScriptEnabled") + @Composable + fun SiteMonitoringWebViewPhp(uiState: SiteMonitorUiState) { + var webView: WebView? by remember { mutableStateOf(null) } + + if (uiState is SiteMonitorUiState.Prepared) { + val model = uiState.model + LaunchedEffect(true) { + webView = WebView(this@SiteMonitorParentActivity).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY + settings.userAgentString = model.userAgent + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + webViewClient = SiteMonitorWebViewClient(this@SiteMonitorParentActivity) + postUrl(WPWebViewActivity.WPCOM_LOGIN_URL, model.addressToLoad.toByteArray()) + } + } + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + if (uiState is SiteMonitorUiState.Prepared) { + LoadingState() + } else { + webView?.let { theWebView -> + AndroidView( + factory = { theWebView }, + modifier = Modifier.fillMaxSize() + ) + } + } + } + } + + @Composable + fun SiteMonitoringWebViewForTabWeb(uiState: SiteMonitorUiState) { + LazyColumn { + item { + when (uiState) { + is SiteMonitorUiState.Preparing -> LoadingState() + is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded -> SiteMonitoringWebViewWeb(uiState) + is SiteMonitorUiState.Error -> SiteMonitorError(uiState) + } + } + } + } @SuppressLint("SetJavaScriptEnabled") @Composable - fun SiteMonitoringWebView(uiState: SiteMonitorUiState) { + fun SiteMonitoringWebViewWeb(uiState: SiteMonitorUiState) { var webView: WebView? by remember { mutableStateOf(null) } if (uiState is SiteMonitorUiState.Prepared) { @@ -203,4 +321,17 @@ val viewModel:SiteMonitorParentViewModel by viewModels() } } } + + @Composable + fun LoadingState() { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + CircularProgressIndicator( + modifier = Modifier.size(48.dp).fillMaxSize() + ) + Text(text = "Loading...", modifier = Modifier.padding(top = 8.dp)) + } + } } From a7a5b4b030875c1118c50eb81a1eccfe6352a9e1 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sun, 28 Jan 2024 17:21:35 -0500 Subject: [PATCH 16/26] Add enum class for each site monitor tab --- .../ui/sitemonitor/SiteMonitorTabItem.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabItem.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabItem.kt new file mode 100644 index 000000000000..5940fd224375 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabItem.kt @@ -0,0 +1,34 @@ +package org.wordpress.android.ui.sitemonitor + +import androidx.annotation.StringRes +import org.wordpress.android.R + +const val METRICS_URL_TEMPLATE = "https://wordpress.com/site-monitoring/{blog}" +const val PHPLOGS_URL_TEMPLATE = "https://wordpress.com/site-monitoring/{blog}/php" +const val WEBSERVERLOGS_URL_TEMPLATE = "https://wordpress.com/site-monitoring/{blog}/web" + +enum class SiteMonitorTabItem( + val route: String, + @StringRes val title: Int, + val urlTemplate: String, + val siteMonitorType: SiteMonitorType +) { + Metrics( + "metrics", + R.string.site_monitoring_tab_title_metrics, + METRICS_URL_TEMPLATE, + SiteMonitorType.METRICS + ), + PHPLogs( + "phplogs", + R.string.site_monitoring_tab_title_php_logs, + PHPLOGS_URL_TEMPLATE, + SiteMonitorType.PHP_LOGS + ), + WebServerLogs( + "webserverlogs", + R.string.site_monitoring_tab_title_web_server_logs, + WEBSERVERLOGS_URL_TEMPLATE, + SiteMonitorType.WEB_SERVER_LOGS + ); +} From b457da1594c961b0074e950b63d5f23467c9c2c0 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sun, 28 Jan 2024 17:22:28 -0500 Subject: [PATCH 17/26] Add a container to host the fragment for each tab --- .../SiteMonitorFragmentContainer.kt | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorFragmentContainer.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorFragmentContainer.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorFragmentContainer.kt new file mode 100644 index 000000000000..eb5eea65c8a1 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorFragmentContainer.kt @@ -0,0 +1,76 @@ +package org.wordpress.android.ui.sitemonitor + +import android.content.Context +import android.view.View +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.viewinterop.AndroidView +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentContainerView +import androidx.fragment.app.FragmentTransaction +import androidx.fragment.app.commit +import androidx.fragment.app.findFragment + +@Suppress("SwallowedException") +@Composable +fun SiteMonitorFragmentContainer( + modifier: Modifier = Modifier, + commit: FragmentTransaction.(containerId: Int) -> Unit +) { + val currentLocalView = LocalView.current + // Using the current view, check if a parent fragment exists. + // This will help ensure that the fragment are nested correctly. + // This assists in saving/restoring the fragments to their proper state + val parentFragment = remember(currentLocalView) { + try { + currentLocalView.findFragment() + } catch (e: IllegalStateException) { + null + } + } + val viewId by rememberSaveable { mutableIntStateOf(View.generateViewId()) } + val container = remember { mutableStateOf(null) } + val viewSection: (Context) -> View = remember(currentLocalView) { + { context -> + FragmentContainerView(context) + .apply { id = viewId } + .also { + val fragmentManager = parentFragment?.childFragmentManager + ?: (context as? FragmentActivity)?.supportFragmentManager + fragmentManager?.commit { commit(it.id) } + container.value = it + } + } + } + AndroidView( + modifier = modifier, + factory = viewSection, + update = {} + ) + + // Be sure to clean up the fragments when the FragmentContainer is disposed + val localContext = LocalContext.current + DisposableEffect(currentLocalView, localContext, container) { + onDispose { + val fragmentManager = parentFragment?.childFragmentManager + ?: (localContext as? FragmentActivity)?.supportFragmentManager + // Use the FragmentContainerView to find the inflated fragment + val existingFragment = fragmentManager?.findFragmentById(container.value?.id ?: 0) + if (existingFragment != null && !fragmentManager.isStateSaved) { + // A composable has been removed from the hierarchy if the state isn't saved + fragmentManager.commit { + remove(existingFragment) + } + } + } + } +} From 2bfdb36602d8cadf642cabe596106885e8ec47b8 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sun, 28 Jan 2024 17:26:58 -0500 Subject: [PATCH 18/26] Add placeholder method to track tab loaded. There is a todo here --- .../wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt index dbaa7b25cebd..f4e7fe8c098d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt @@ -1,5 +1,6 @@ package org.wordpress.android.ui.sitemonitor +import android.util.Log import org.wordpress.android.WordPress import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.ui.WPWebViewActivity @@ -33,6 +34,11 @@ class SiteMonitorUtils @Inject constructor( } } + fun trackTabLoaded(siteMonitorType: SiteMonitorType) { + // todo: need to set this up properly with track events + Log.i(javaClass.simpleName, "track TabLoaded with $siteMonitorType") + } + companion object { const val HTTP_PATTERN = "(https?://)" const val PHP_LOGS_PATTERN = "/php" From d6329cf27d677b2c1bf3db560117a96c9f4f0b64 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sun, 28 Jan 2024 17:28:10 -0500 Subject: [PATCH 19/26] Add a composable to hold the current screen so we can go back and restore state on tab to tab --- .../sitemonitor/SiteMonitorTabNavigation.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabNavigation.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabNavigation.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabNavigation.kt new file mode 100644 index 000000000000..023e651a66d5 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabNavigation.kt @@ -0,0 +1,20 @@ +package org.wordpress.android.ui.sitemonitor + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import androidx.compose.ui.Modifier + +@Composable +fun SiteMonitorTabNavigation ( + currentScreen: T, + modifier: Modifier = Modifier, + content: @Composable (T) -> Unit +) { + val saveableStateHolder = rememberSaveableStateHolder() + Box(modifier) { + saveableStateHolder.SaveableStateProvider(currentScreen) { + content(currentScreen) + } + } +} From 869251a8fa6b2731588de4ab701b82994d3d8ad6 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sun, 28 Jan 2024 17:29:00 -0500 Subject: [PATCH 20/26] Add a composable for the site monitor tab header --- .../ui/sitemonitor/SiteMonitorTabHeader.kt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabHeader.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabHeader.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabHeader.kt new file mode 100644 index 000000000000..1bf493f7c49d --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabHeader.kt @@ -0,0 +1,51 @@ +package org.wordpress.android.ui.sitemonitor + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.sp + +@Composable +fun SiteMonitorTabHeader(navController: (String) -> Unit) { + var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } + val tabs = listOf( + SiteMonitorTabItem.Metrics, + SiteMonitorTabItem.PHPLogs, + SiteMonitorTabItem.WebServerLogs + ) + TabRow( + selectedTabIndex = selectedTabIndex, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + ) { + tabs.forEachIndexed { index, item -> + Tab( + text = { + Column (horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = stringResource(item.title), + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + }, + selected = selectedTabIndex == index, + onClick = { + selectedTabIndex = index + navController(item.route) + }, + ) + } + } +} From 3cabeb3bb4693d383b2b2dbcb6cda0267d406bca Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sun, 28 Jan 2024 17:30:13 -0500 Subject: [PATCH 21/26] Add fragment and viewmodel pair that will host the webview for each tab. --- .../ui/sitemonitor/SiteMonitorTabFragment.kt | 184 ++++++++++++++++++ .../ui/sitemonitor/SiteMonitorTabViewModel.kt | 120 ++++++++++++ 2 files changed, 304 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabFragment.kt create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModel.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabFragment.kt new file mode 100644 index 000000000000..27fbf39c3f12 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabFragment.kt @@ -0,0 +1,184 @@ +package org.wordpress.android.ui.sitemonitor + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.WebView +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +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.platform.ComposeView +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import org.wordpress.android.WordPress +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.WPWebViewActivity +import org.wordpress.android.ui.compose.utils.uiStringText + +@AndroidEntryPoint +class SiteMonitorTabFragment : Fragment(), SiteMonitorWebViewClient.SiteMonitorWebViewClientListener { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View = ComposeView(requireContext()).apply { + setContent { + TheContent() + } + } + + private val viewModel: SiteMonitorTabViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initViewModel(getSiteMonitorType(), getUrlTemplate(), getSite()) + } + + @Suppress("DEPRECATION") + private fun getSite(): SiteModel { + return requireNotNull(arguments?.getSerializable(WordPress.SITE)) as SiteModel + } + + private fun getUrlTemplate(): String { + return requireNotNull(arguments?.getString(KEY_URL_TEMPLATE)) + } + + @Suppress("DEPRECATION") + private fun getSiteMonitorType(): SiteMonitorType { + return requireNotNull(arguments?.getSerializable(KEY_SITE_MONITOR_TYPE)) as SiteMonitorType + } + + private fun initViewModel(type: SiteMonitorType, urlTemplate: String, site: SiteModel) { + viewModel.start(type, urlTemplate, site) + } + + override fun onWebViewPageLoaded(url: String) = viewModel.onUrlLoaded() + + override fun onWebViewReceivedError(url: String) = viewModel.onWebViewError() + + companion object { + const val KEY_URL_TEMPLATE = "KEY_URL" + const val KEY_SITE_MONITOR_TYPE = "KEY_SITE_MONITOR_TYPE" + fun newInstance(url: String, type: SiteMonitorType, site: SiteModel): Fragment { + val fragment = SiteMonitorTabFragment() + val argument = Bundle() + argument.putString(KEY_URL_TEMPLATE, url) + argument.putSerializable(KEY_SITE_MONITOR_TYPE, type) + argument.putSerializable(WordPress.SITE, site) + fragment.arguments = argument + return fragment + } + } + + @Composable + private fun TheContent() { + val uiState by viewModel.uiState.collectAsState() + when (uiState) { + is SiteMonitorUiState.Preparing -> LoadingState() + is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded -> TheWebView(uiState) + is SiteMonitorUiState.Error -> SiteMonitorError(uiState as SiteMonitorUiState.Error) + } + } + + @Composable + fun SiteMonitorError(error: SiteMonitorUiState.Error) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .padding(20.dp) + .fillMaxWidth() + .fillMaxHeight(), + ) { + androidx.compose.material.Text( + text = uiStringText(uiString = error.title), + style = androidx.compose.material.MaterialTheme.typography.h5, + textAlign = TextAlign.Center + ) + androidx.compose.material.Text( + text = uiStringText(uiString = error.description), + style = androidx.compose.material.MaterialTheme.typography.body1, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 8.dp) + ) + if (error.button != null) { + Button( + modifier = Modifier.padding(top = 8.dp), + onClick = error.button.click + ) { + androidx.compose.material.Text(text = uiStringText(uiString = error.button.text)) + } + } + } + } + + @SuppressLint("SetJavaScriptEnabled") + @Composable + private fun TheWebView(uiState: SiteMonitorUiState) { + var webView: WebView? by remember { mutableStateOf(null) } + + if (uiState is SiteMonitorUiState.Prepared) { + val model = uiState.model + LaunchedEffect(true) { + webView = WebView(requireContext()).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY + settings.userAgentString = model.userAgent + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + webViewClient = SiteMonitorWebViewClient(this@SiteMonitorTabFragment) + postUrl(WPWebViewActivity.WPCOM_LOGIN_URL, model.addressToLoad.toByteArray()) + } + } + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + if (uiState is SiteMonitorUiState.Prepared) { + LoadingState() + } else { + webView?.let { theWebView -> + AndroidView( + factory = { theWebView }, + modifier = Modifier.fillMaxSize() + ) + } + } + } + } +} + +@Composable +fun LoadingState(modifier: Modifier = Modifier) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier.fillMaxSize() + ) { + CircularProgressIndicator() + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModel.kt new file mode 100644 index 000000000000..617b984d6f56 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModel.kt @@ -0,0 +1,120 @@ +package org.wordpress.android.ui.sitemonitor + +import android.text.TextUtils +import android.util.Log +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.AccountStore +import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.util.NetworkUtilsWrapper +import org.wordpress.android.viewmodel.ScopedViewModel +import javax.inject.Inject +import javax.inject.Named + +@HiltViewModel +class SiteMonitorTabViewModel @Inject constructor( + @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, + private val networkUtilsWrapper: NetworkUtilsWrapper, + private val accountStore: AccountStore, + private val mapper: SiteMonitorMapper, + private val siteMonitorUtils: SiteMonitorUtils, + private val siteStore: SiteStore, +) : ScopedViewModel(bgDispatcher) { + private lateinit var site: SiteModel + private lateinit var siteMonitorType: SiteMonitorType + private lateinit var urlTemplate: String + + private val _uiState = MutableStateFlow(SiteMonitorUiState.Preparing) + val uiState: StateFlow = _uiState + + fun start(type: SiteMonitorType, urlTemplate: String, site: SiteModel) { + Log.i("Track", "TheViewModel start with $urlTemplate and $type") + this.siteMonitorType = type + this.urlTemplate = urlTemplate + this.site = site + + loadView() + } + + private fun loadView() { + postUiState(SiteMonitorUiState.Preparing) + + if (!checkForInternetConnectivityAndPostErrorIfNeeded()) return + + if (!validateAndPostErrorIfNeeded()) return + + assembleAndShowSiteMonitor() + } + + private fun checkForInternetConnectivityAndPostErrorIfNeeded() : Boolean { + if (networkUtilsWrapper.isNetworkAvailable()) return true + postUiState(mapper.toNoNetworkError(this@SiteMonitorTabViewModel::loadView)) + return false + } + + private fun validateAndPostErrorIfNeeded(): Boolean { + if (accountStore.account.userName.isNullOrEmpty() || accountStore.accessToken.isNullOrEmpty()) { + postUiState(mapper.toGenericError(this@SiteMonitorTabViewModel::loadView)) + return false + } + return true + } + + private fun assembleAndShowSiteMonitor() { + val sanitizedUrl = siteMonitorUtils.sanitizeSiteUrl(site.url) + val url = urlTemplate.replace("{blog}", sanitizedUrl) + + val addressToLoad = prepareAddressToLoad(url) + postUiState(mapper.toPrepared(url, addressToLoad, siteMonitorType)) + } + + private fun prepareAddressToLoad(url: String): String { + val username = accountStore.account.userName + val accessToken = accountStore.accessToken + + var addressToLoad = url + + // Custom domains are not properly authenticated due to a server side(?) issue, so this gets around that + if (!addressToLoad.contains(SiteMonitorParentViewModel.WPCOM_DOMAIN)) { + val wpComSites: List = siteStore.wPComSites + for (siteModel in wpComSites) { + // Only replace the url if we know the unmapped url and if it's a custom domain + if (!TextUtils.isEmpty(siteModel.unmappedUrl) + && !siteModel.url.contains(SiteMonitorParentViewModel.WPCOM_DOMAIN) + ) { + addressToLoad = addressToLoad.replace(siteModel.url, siteModel.unmappedUrl) + } + } + } + return siteMonitorUtils.getAuthenticationPostData( + WPCOM_LOGIN_URL, + addressToLoad, + username, + "", + accessToken?:"" + ) + } + + private fun postUiState(state: SiteMonitorUiState) { + launch { + _uiState.value = state + } + } + + fun onUrlLoaded() { + siteMonitorUtils.trackTabLoaded(siteMonitorType) + postUiState(SiteMonitorUiState.Loaded) + } + + fun onWebViewError() { + postUiState(mapper.toGenericError(this@SiteMonitorTabViewModel::loadView)) + } + + companion object { + const val WPCOM_LOGIN_URL = "https://wordpress.com/wp-login.php" + } +} From 195b6792800c5bdd69eece8ba012e789a1906d98 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sun, 28 Jan 2024 17:32:02 -0500 Subject: [PATCH 22/26] Refactor: Use fragments for tab navigation, eliminate the viewModel, save tab state. --- .../sitemonitor/SiteMonitorParentActivity.kt | 344 ++++-------------- 1 file changed, 69 insertions(+), 275 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt index 3bae47007aa0..7291b52263ab 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt @@ -2,81 +2,78 @@ package org.wordpress.android.ui.sitemonitor import android.annotation.SuppressLint import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import android.webkit.WebView +import android.util.SparseArray import androidx.activity.compose.setContent -import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Button -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold -import androidx.compose.material.TabRow -import androidx.compose.material.Text -import androidx.compose.material3.Tab +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.ui.WPWebViewActivity import org.wordpress.android.ui.compose.components.MainTopAppBar import org.wordpress.android.ui.compose.components.NavigationIcons import org.wordpress.android.ui.compose.theme.AppTheme -import org.wordpress.android.ui.compose.utils.uiStringText -import org.wordpress.android.ui.sitemonitor.SiteMonitorWebViewClient.SiteMonitorWebViewClientListener import org.wordpress.android.util.extensions.getSerializableExtraCompat @AndroidEntryPoint -class SiteMonitorParentActivity: AppCompatActivity(), SiteMonitorWebViewClientListener { - override fun onWebViewPageLoaded(url: String) = viewModel.onUrlLoaded(url) - - override fun onWebViewReceivedError(url: String) = viewModel.onWebViewError(url) - - val viewModel:SiteMonitorParentViewModel by viewModels() +class SiteMonitorParentActivity: AppCompatActivity() { + private var savedStateSparseArray = SparseArray() + private var currentSelectItemId = 0 + @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (savedInstanceState != null) { + savedStateSparseArray = savedInstanceState.getSparseParcelableArray( + SAVED_STATE_CONTAINER_KEY + ) + ?: savedStateSparseArray + currentSelectItemId = savedInstanceState.getInt(SAVED_STATE_CURRENT_TAB_KEY) + } setContent { AppTheme { - viewModel.start(getSite()) - SiteMonitorScreen() + Surface( + modifier = Modifier.fillMaxSize(), + ) { + SiteMonitorScreen() + } } } } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putSparseParcelableArray(SAVED_STATE_CONTAINER_KEY, savedStateSparseArray) + outState.putInt(SAVED_STATE_CURRENT_TAB_KEY, currentSelectItemId) + } + private fun getSite(): SiteModel { return requireNotNull(intent.getSerializableExtraCompat(WordPress.SITE)) as SiteModel } + companion object { + const val SAVED_STATE_CONTAINER_KEY = "ContainerKey" + const val SAVED_STATE_CURRENT_TAB_KEY = "CurrentTabKey" + } + @Composable @SuppressLint("UnusedMaterialScaffoldPaddingParameter") - fun SiteMonitorScreen(modifier: Modifier = Modifier, - viewModel: SiteMonitorParentViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) { - val uiStates by viewModel.uiStates.collectAsState() + fun SiteMonitorScreen() { + var selectedTab by rememberSaveable { mutableStateOf(SiteMonitorTabItem.Metrics.route) } Scaffold( topBar = { MainTopAppBar( @@ -84,254 +81,51 @@ class SiteMonitorParentActivity: AppCompatActivity(), SiteMonitorWebViewClientLi navigationIcon = NavigationIcons.BackIcon, onNavigationIconClick = onBackPressedDispatcher::onBackPressed, ) - }, - content = { - TabScreen(modifier = modifier, uiStates) - } - ) - } - - @Composable - @SuppressLint("UnusedMaterialScaffoldPaddingParameter") - fun TabScreen(modifier: Modifier = Modifier, uiStates: Map) { - var tabIndex by remember { mutableIntStateOf(0) } - - val tabs = listOf( - R.string.site_monitoring_tab_title_metrics, - R.string.site_monitoring_tab_title_php_logs, - R.string.site_monitoring_tab_title_web_server_logs - ) - - val tabsToType = mapOf( - 0 to SiteMonitorType.METRICS, - 1 to SiteMonitorType.PHP_LOGS, - 2 to SiteMonitorType.WEB_SERVER_LOGS - ) - - Column(modifier = modifier.fillMaxWidth()) { - TabRow( - selectedTabIndex = tabIndex, - backgroundColor = MaterialTheme.colors.surface, - contentColor = MaterialTheme.colors.onSurface, - ) { - tabs.forEachIndexed { index, title -> - Tab(text = { Text(stringResource(id = title)) }, - selected = tabIndex == index, - onClick = { - tabIndex = index - viewModel.onTabSelected(tabsToType[index]) - } - ) - } } - val siteMonitorType = tabsToType[tabIndex] ?: SiteMonitorType.METRICS - val uiState = uiStates[siteMonitorType] as SiteMonitorUiState - when(siteMonitorType) { - SiteMonitorType.METRICS -> SiteMonitoringWebViewForMetric(uiState) - SiteMonitorType.PHP_LOGS -> SiteMonitoringWebViewForTabPhp(uiState) - SiteMonitorType.WEB_SERVER_LOGS -> SiteMonitoringWebViewForTabWeb(uiState) - } - } - } - - @Composable - fun SiteMonitoringWebViewForMetric(uiState: SiteMonitorUiState) { - LazyColumn { - item { - when (uiState) { - is SiteMonitorUiState.Preparing -> LoadingState() - is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded -> SiteMonitoringWebViewMetric(uiState) - is SiteMonitorUiState.Error -> SiteMonitorError(uiState) - } - } - } - } - - @SuppressLint("SetJavaScriptEnabled") - @Composable - fun SiteMonitoringWebViewMetric(uiState: SiteMonitorUiState) { - var webView: WebView? by remember { mutableStateOf(null) } - - if (uiState is SiteMonitorUiState.Prepared) { - val model = uiState.model - LaunchedEffect(true) { - webView = WebView(this@SiteMonitorParentActivity).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY - settings.userAgentString = model.userAgent - settings.javaScriptEnabled = true - settings.domStorageEnabled = true - webViewClient = SiteMonitorWebViewClient(this@SiteMonitorParentActivity) - postUrl(WPWebViewActivity.WPCOM_LOGIN_URL, model.addressToLoad.toByteArray()) + ) { padding -> + Column(modifier = Modifier.padding(padding)) { + SiteMonitorTabHeader { clickTab -> + selectedTab = clickTab } - } - } - - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - if (uiState is SiteMonitorUiState.Prepared) { - LoadingState() - } else { - webView?.let { theWebView -> - AndroidView( - factory = { theWebView }, - modifier = Modifier.fillMaxSize() + SiteMonitorTabNavigation(selectedTab) { selectedTab -> + val item = enumValues().find { + it.route == selectedTab + } ?: SiteMonitorTabItem.Metrics + + SiteMonitorFragmentContainer( + modifier = Modifier.fillMaxSize(), + commit = getCommitFunction( + SiteMonitorTabFragment.newInstance(item.urlTemplate, item.siteMonitorType, getSite()), + item.route + ) ) } } } } - @Composable - fun SiteMonitorError(error: SiteMonitorUiState.Error) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier - .padding(20.dp) - .fillMaxWidth() - .fillMaxHeight(), - ) { - Text( - text = uiStringText(uiString = error.title), - style = MaterialTheme.typography.h5, - textAlign = TextAlign.Center - ) - Text( - text = uiStringText(uiString = error.description), - style = MaterialTheme.typography.body1, - textAlign = TextAlign.Center, - modifier = Modifier.padding(top = 8.dp) - ) - if (error.button != null) { - Button( - modifier = Modifier.padding(top = 8.dp), - onClick = error.button.click - ) { - Text(text = uiStringText(uiString = error.button.text)) - } - } - } - } - @Composable - fun SiteMonitoringWebViewForTabPhp(uiState: SiteMonitorUiState) { - LazyColumn { - item { - when (uiState) { - is SiteMonitorUiState.Preparing -> LoadingState() - is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded -> SiteMonitoringWebViewPhp(uiState) - is SiteMonitorUiState.Error -> SiteMonitorError(uiState) - } - } - } - } - @SuppressLint("SetJavaScriptEnabled") - @Composable - fun SiteMonitoringWebViewPhp(uiState: SiteMonitorUiState) { - var webView: WebView? by remember { mutableStateOf(null) } - - if (uiState is SiteMonitorUiState.Prepared) { - val model = uiState.model - LaunchedEffect(true) { - webView = WebView(this@SiteMonitorParentActivity).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY - settings.userAgentString = model.userAgent - settings.javaScriptEnabled = true - settings.domStorageEnabled = true - webViewClient = SiteMonitorWebViewClient(this@SiteMonitorParentActivity) - postUrl(WPWebViewActivity.WPCOM_LOGIN_URL, model.addressToLoad.toByteArray()) - } - } - } - - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - if (uiState is SiteMonitorUiState.Prepared) { - LoadingState() - } else { - webView?.let { theWebView -> - AndroidView( - factory = { theWebView }, - modifier = Modifier.fillMaxSize() - ) - } - } + private fun getCommitFunction( + fragment : Fragment, + tag: String + ): FragmentTransaction.(containerId: Int) -> Unit = + { + saveAndRetrieveFragment(supportFragmentManager, it, fragment) + replace(it, fragment, tag) } - } - @Composable - fun SiteMonitoringWebViewForTabWeb(uiState: SiteMonitorUiState) { - LazyColumn { - item { - when (uiState) { - is SiteMonitorUiState.Preparing -> LoadingState() - is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded -> SiteMonitoringWebViewWeb(uiState) - is SiteMonitorUiState.Error -> SiteMonitorError(uiState) - } - } - } - } - @SuppressLint("SetJavaScriptEnabled") - @Composable - fun SiteMonitoringWebViewWeb(uiState: SiteMonitorUiState) { - var webView: WebView? by remember { mutableStateOf(null) } - - if (uiState is SiteMonitorUiState.Prepared) { - val model = uiState.model - LaunchedEffect(true) { - webView = WebView(this@SiteMonitorParentActivity).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY - settings.userAgentString = model.userAgent - settings.javaScriptEnabled = true - settings.domStorageEnabled = true - webViewClient = SiteMonitorWebViewClient(this@SiteMonitorParentActivity) - postUrl(WPWebViewActivity.WPCOM_LOGIN_URL, model.addressToLoad.toByteArray()) - } - } - } - - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - if (uiState is SiteMonitorUiState.Prepared) { - LoadingState() - } else { - webView?.let { theWebView -> - AndroidView( - factory = { theWebView }, - modifier = Modifier.fillMaxSize() - ) - } - } - } - } - - @Composable - fun LoadingState() { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize() - ) { - CircularProgressIndicator( - modifier = Modifier.size(48.dp).fillMaxSize() + private fun saveAndRetrieveFragment( + supportFragmentManager: FragmentManager, + tabId: Int, + fragment: Fragment + ) { + val currentFragment = supportFragmentManager.findFragmentById(currentSelectItemId) + if (currentFragment != null) { + savedStateSparseArray.put( + currentSelectItemId, + supportFragmentManager.saveFragmentInstanceState(currentFragment) ) - Text(text = "Loading...", modifier = Modifier.padding(top = 8.dp)) } + currentSelectItemId = tabId + fragment.setInitialSavedState(savedStateSparseArray[currentSelectItemId]) } } From fd628cbc00482170f5559b492ef32dc9aed5a8c9 Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sun, 28 Jan 2024 17:35:46 -0500 Subject: [PATCH 23/26] Refactor: remove references to SiteMonitorParentViewModel --- .../android/ui/sitemonitor/SiteMonitorTabViewModel.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModel.kt index 617b984d6f56..aaae3507f424 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModel.kt @@ -79,12 +79,12 @@ class SiteMonitorTabViewModel @Inject constructor( var addressToLoad = url // Custom domains are not properly authenticated due to a server side(?) issue, so this gets around that - if (!addressToLoad.contains(SiteMonitorParentViewModel.WPCOM_DOMAIN)) { + if (!addressToLoad.contains(WPCOM_DOMAIN)) { val wpComSites: List = siteStore.wPComSites for (siteModel in wpComSites) { // Only replace the url if we know the unmapped url and if it's a custom domain if (!TextUtils.isEmpty(siteModel.unmappedUrl) - && !siteModel.url.contains(SiteMonitorParentViewModel.WPCOM_DOMAIN) + && !siteModel.url.contains(WPCOM_DOMAIN) ) { addressToLoad = addressToLoad.replace(siteModel.url, siteModel.unmappedUrl) } @@ -116,5 +116,6 @@ class SiteMonitorTabViewModel @Inject constructor( companion object { const val WPCOM_LOGIN_URL = "https://wordpress.com/wp-login.php" + const val WPCOM_DOMAIN = ".wordpress.com" } } From aa95c501e3dfdb1ba149a7d2447f68d7eb13210b Mon Sep 17 00:00:00 2001 From: Annmarie Ziegler Date: Sun, 28 Jan 2024 17:35:55 -0500 Subject: [PATCH 24/26] Delete classes --- .../sitemonitor/SiteMonitorParentViewModel.kt | 141 ------------------ .../SiteMonitorParentViewModelTest.kt | 36 ----- 2 files changed, 177 deletions(-) delete mode 100644 WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt delete mode 100644 WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModelTest.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt deleted file mode 100644 index ef09fe2a216d..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt +++ /dev/null @@ -1,141 +0,0 @@ -package org.wordpress.android.ui.sitemonitor - -import android.text.TextUtils -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.fluxc.store.AccountStore -import org.wordpress.android.fluxc.store.SiteStore -import org.wordpress.android.modules.BG_THREAD -import org.wordpress.android.util.NetworkUtilsWrapper -import org.wordpress.android.viewmodel.ScopedViewModel -import javax.inject.Inject -import javax.inject.Named - -@HiltViewModel -class SiteMonitorParentViewModel @Inject constructor( - @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, - private val networkUtilsWrapper: NetworkUtilsWrapper, - private val accountStore: AccountStore, - private val mapper: SiteMonitorMapper, - private val siteMonitorUtils: SiteMonitorUtils, - private val siteStore: SiteStore, -) : ScopedViewModel(bgDispatcher) { - private lateinit var site: SiteModel - - private val metricUrlTemplate = "https://wordpress.com/site-monitoring/{blog}" - private val phpLogsUrlTemplate = "https://wordpress.com/site-monitoring/{blog}/php" - private val webServerLogsUrlTemplate = "https://wordpress.com/site-monitoring/{blog}/web" - - private val _uiStates = MutableStateFlow>(emptyMap()) - val uiStates: StateFlow> = _uiStates - - - fun start(site: SiteModel) { - this.site = site - siteMonitorUtils.trackActivityLaunched() - - loadViews() - } - - private fun loadViews(siteMonitorType: SiteMonitorType? = null) { - if (siteMonitorType != null) { - loadIndividualView(siteMonitorType) - return - } - - SiteMonitorType.entries.forEach { type -> loadIndividualView(type) } - } - - private fun loadIndividualView(siteMonitorType: SiteMonitorType) { - postUiState(siteMonitorType, SiteMonitorUiState.Preparing) - - if (!checkForInternetConnectivityAndPostErrorIfNeeded(siteMonitorType)) return - - if (!validateAndPostErrorIfNeeded(siteMonitorType)) return - - assembleAndShowSiteMonitor(siteMonitorType) - } - - private fun assembleAndShowSiteMonitor(type: SiteMonitorType) { - val sanitizedUrl = siteMonitorUtils.sanitizeSiteUrl(site.url) - val url = when (type) { - SiteMonitorType.METRICS -> metricUrlTemplate - SiteMonitorType.PHP_LOGS -> phpLogsUrlTemplate - SiteMonitorType.WEB_SERVER_LOGS -> webServerLogsUrlTemplate - }.replace("{blog}", sanitizedUrl) - - val addressToLoad = prepareAddressToLoad(url) - val uiState = mapper.toPrepared(url, addressToLoad, type) - postUiState(type, uiState) - } - - private fun prepareAddressToLoad(url: String): String { - val username = accountStore.account.userName - val accessToken = accountStore.accessToken - - var addressToLoad = url - - // Custom domains are not properly authenticated due to a server side(?) issue, so this gets around that - if (!addressToLoad.contains(WPCOM_DOMAIN)) { - val wpComSites: List = siteStore.wPComSites - for (siteModel in wpComSites) { - // Only replace the url if we know the unmapped url and if it's a custom domain - if (!TextUtils.isEmpty(siteModel.unmappedUrl) - && !siteModel.url.contains(WPCOM_DOMAIN) - ) { - addressToLoad = addressToLoad.replace(siteModel.url, siteModel.unmappedUrl) - } - } - } - return siteMonitorUtils.getAuthenticationPostData( - WPCOM_LOGIN_URL, - addressToLoad, - username, - "", - accessToken?:"" - ) - } - private fun checkForInternetConnectivityAndPostErrorIfNeeded(type: SiteMonitorType) : Boolean { - if (networkUtilsWrapper.isNetworkAvailable()) return true - postUiState(type, mapper.toNoNetworkError(this@SiteMonitorParentViewModel::loadViews)) - return false - } - - private fun validateAndPostErrorIfNeeded(type: SiteMonitorType): Boolean { - if (accountStore.account.userName.isNullOrEmpty() || accountStore.accessToken.isNullOrEmpty()) { - postUiState(type, mapper.toGenericError(this@SiteMonitorParentViewModel::loadViews)) - return false - } - return true - } - - private fun postUiState(type: SiteMonitorType, uiState: SiteMonitorUiState) { - launch { - _uiStates.value = _uiStates.value.toMutableMap().apply { - this[type] = uiState - } - } - } - - fun onUrlLoaded(url: String) { - val type = siteMonitorUtils.urlToType(url) - postUiState(type, SiteMonitorUiState.Loaded) - } - - fun onWebViewError(url: String) { - val type = siteMonitorUtils.urlToType(url) - postUiState(type, mapper.toGenericError(this@SiteMonitorParentViewModel::loadViews)) - } - - fun onTabSelected(siteMonitorType: SiteMonitorType?) { - loadViews(siteMonitorType) - } - - companion object { - const val WPCOM_LOGIN_URL = "https://wordpress.com/wp-login.php" - const val WPCOM_DOMAIN = ".wordpress.com" - } -} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModelTest.kt deleted file mode 100644 index 7f2031d8b1c5..000000000000 --- a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModelTest.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.wordpress.android.ui.sitemonitor - -import kotlinx.coroutines.ExperimentalCoroutinesApi -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.junit.MockitoJUnitRunner -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.wordpress.android.BaseUnitTest -import org.wordpress.android.analytics.AnalyticsTracker -import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper - -@ExperimentalCoroutinesApi -@RunWith(MockitoJUnitRunner::class) -class SiteMonitorParentViewModelTest: BaseUnitTest(){ - @Mock - private lateinit var analyticsTrackerWrapper: AnalyticsTrackerWrapper - - private lateinit var viewModel: SiteMonitorParentViewModel - - @Before - fun setUp() { - viewModel = SiteMonitorParentViewModel(testDispatcher(), analyticsTrackerWrapper) - } - - @Test - fun `when viewmodel is started, then screen shown tracking is done`() { - val site = mock() - viewModel.start(site) - - verify(analyticsTrackerWrapper).track(AnalyticsTracker.Stat.SITE_MONITORING_SCREEN_SHOWN) - } -} From 9b85571d04b0d13610c7030f48aba2ffd58184b4 Mon Sep 17 00:00:00 2001 From: Ajesh R Pai Date: Mon, 29 Jan 2024 12:17:38 +0530 Subject: [PATCH 25/26] * Renames: Content to SiteMonitorTabContent --- .../android/ui/sitemonitor/SiteMonitorTabFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabFragment.kt index 27fbf39c3f12..c0cf2b9db50c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabFragment.kt @@ -43,7 +43,7 @@ class SiteMonitorTabFragment : Fragment(), SiteMonitorWebViewClient.SiteMonitorW savedInstanceState: Bundle? ): View = ComposeView(requireContext()).apply { setContent { - TheContent() + SiteMonitorTabContent() } } @@ -91,7 +91,7 @@ class SiteMonitorTabFragment : Fragment(), SiteMonitorWebViewClient.SiteMonitorW } @Composable - private fun TheContent() { + private fun SiteMonitorTabContent() { val uiState by viewModel.uiState.collectAsState() when (uiState) { is SiteMonitorUiState.Preparing -> LoadingState() From 9362ef000e96538e21fe2499c4f3af92c0a4751a Mon Sep 17 00:00:00 2001 From: Ajesh R Pai Date: Mon, 29 Jan 2024 12:17:56 +0530 Subject: [PATCH 26/26] * Renames: WebView to SiteMonitorWebView --- .../android/ui/sitemonitor/SiteMonitorTabFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabFragment.kt index c0cf2b9db50c..8fb61cc8ed03 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabFragment.kt @@ -95,7 +95,7 @@ class SiteMonitorTabFragment : Fragment(), SiteMonitorWebViewClient.SiteMonitorW val uiState by viewModel.uiState.collectAsState() when (uiState) { is SiteMonitorUiState.Preparing -> LoadingState() - is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded -> TheWebView(uiState) + is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded -> SiteMonitorWebView(uiState) is SiteMonitorUiState.Error -> SiteMonitorError(uiState as SiteMonitorUiState.Error) } } @@ -134,7 +134,7 @@ class SiteMonitorTabFragment : Fragment(), SiteMonitorWebViewClient.SiteMonitorW @SuppressLint("SetJavaScriptEnabled") @Composable - private fun TheWebView(uiState: SiteMonitorUiState) { + private fun SiteMonitorWebView(uiState: SiteMonitorUiState) { var webView: WebView? by remember { mutableStateOf(null) } if (uiState is SiteMonitorUiState.Prepared) {