diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 0e1b80833633..e2e517ec71ba 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -2,7 +2,8 @@ 24.2 ----- - +* [**] Fix editor crash occurring on large posts [https://github.com/wordpress-mobile/WordPress-Android/pull/20046] +* [*] [Jetpack-only] Site Monitoring: Add Metrics, PHP Logs, and Web Server Logs under Site Monitoring [https://github.com/wordpress-mobile/WordPress-Android/issues/20067] 24.1 ----- diff --git a/WordPress/build.gradle b/WordPress/build.gradle index ac98ada035c5..c911d5d2c816 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -387,10 +387,6 @@ dependencies { } implementation "org.wordpress:persistentedittext:$wordPressPersistentEditTextVersion" - // To enable Stetho, a debug bridge that enables the Chrome Developer Tools for debug purposes. - debugImplementation "com.facebook.stetho:stetho:$stethoVersion" - debugImplementation "com.facebook.stetho:stetho-okhttp3:$stethoVersion" - implementation "androidx.arch.core:core-common:$androidxArchCoreVersion" implementation "androidx.arch.core:core-runtime:$androidxArchCoreVersion" implementation "com.google.code.gson:gson:$googleGsonVersion" @@ -558,6 +554,16 @@ dependencies { // - Jetpack Compose - UI Tests androidTestImplementation "androidx.compose.ui:ui-test-junit4" implementation "androidx.compose.material3:material3:$androidxComposeMaterial3Version" + + // - Flipper + debugImplementation ("com.facebook.flipper:flipper:$flipperVersion") { + exclude group:'org.jetbrains.kotlinx', module:'kotlinx-serialization-json-jvm' + } + debugImplementation "com.facebook.soloader:soloader:$soLoaderVersion" + debugImplementation ("com.facebook.flipper:flipper-network-plugin:$flipperVersion"){ + exclude group:'org.jetbrains.kotlinx', module:'kotlinx-serialization-json-jvm' + } + releaseImplementation "com.facebook.flipper:flipper-noop:$flipperVersion" } configurations.all { diff --git a/WordPress/src/debug/AndroidManifest.xml b/WordPress/src/debug/AndroidManifest.xml index e961d134552f..f8c9e84b0392 100644 --- a/WordPress/src/debug/AndroidManifest.xml +++ b/WordPress/src/debug/AndroidManifest.xml @@ -38,5 +38,7 @@ android:name=".ui.debug.preferences.DebugSharedPreferenceFlagsActivity" android:label="@string/debug_settings_debug_flags_screen" android:theme="@style/WordPress.NoActionBar" /> + diff --git a/WordPress/src/debug/java/org/wordpress/android/WordPressDebug.java b/WordPress/src/debug/java/org/wordpress/android/WordPressDebug.java index 18c86dd485b9..8672794c9767 100644 --- a/WordPress/src/debug/java/org/wordpress/android/WordPressDebug.java +++ b/WordPress/src/debug/java/org/wordpress/android/WordPressDebug.java @@ -2,7 +2,14 @@ import android.os.StrictMode; -import com.facebook.stetho.Stetho; +import com.facebook.flipper.android.AndroidFlipperClient; +import com.facebook.flipper.android.utils.FlipperUtils; +import com.facebook.flipper.core.FlipperClient; +import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; +import com.facebook.flipper.plugins.inspector.DescriptorMapping; +import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; +import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; +import com.facebook.soloader.SoLoader; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; @@ -11,14 +18,23 @@ @HiltAndroidApp public class WordPressDebug extends WordPressApp { + public static final NetworkFlipperPlugin NETWORK_FLIPPER_PLUGIN = new NetworkFlipperPlugin(); @Override public void onCreate() { super.onCreate(); // enableStrictMode() - // Init Stetho - Stetho.initializeWithDefaults(this); + // init Flipper + SoLoader.init(this, false); + + if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) { + final FlipperClient client = AndroidFlipperClient.getInstance(this); + client.addPlugin(new InspectorFlipperPlugin(this, DescriptorMapping.withDefaults())); + client.addPlugin(NETWORK_FLIPPER_PLUGIN); + client.addPlugin(new DatabasesFlipperPlugin(this)); + client.start(); + } } /** diff --git a/WordPress/src/debug/java/org/wordpress/android/modules/InterceptorModule.java b/WordPress/src/debug/java/org/wordpress/android/modules/InterceptorModule.java index cba40b8795c0..a3fff589ff72 100644 --- a/WordPress/src/debug/java/org/wordpress/android/modules/InterceptorModule.java +++ b/WordPress/src/debug/java/org/wordpress/android/modules/InterceptorModule.java @@ -1,6 +1,8 @@ package org.wordpress.android.modules; -import com.facebook.stetho.okhttp3.StethoInterceptor; +import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; + +import org.wordpress.android.WordPressDebug; import javax.inject.Named; @@ -11,11 +13,12 @@ import dagger.multibindings.IntoSet; import okhttp3.Interceptor; + @InstallIn(SingletonComponent.class) @Module public class InterceptorModule { @Provides @IntoSet @Named("network-interceptors") - public Interceptor provideStethoInterceptor() { - return new StethoInterceptor(); + public Interceptor provideFlipperInterceptor() { + return new FlipperOkhttpInterceptor(WordPressDebug.NETWORK_FLIPPER_PLUGIN); } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/personalization/PersonalizationActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/personalization/PersonalizationActivity.kt index 94e60930694d..ac9ab5de738d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/personalization/PersonalizationActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/personalization/PersonalizationActivity.kt @@ -107,7 +107,7 @@ class PersonalizationActivity : AppCompatActivity() { contentColor = MaterialTheme.colors.onSurface, ) { tabs.forEachIndexed { index, title -> - Tab(text = { Text(stringResource(id = title)) }, + Tab(text = { Text(stringResource(id = title).uppercase()) }, selected = tabIndex == index, onClick = { tabIndex = index } ) 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 deleted file mode 100644 index eb5eea65c8a1..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorFragmentContainer.kt +++ /dev/null @@ -1,76 +0,0 @@ -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) - } - } - } - } -} 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 9a9c52b4920b..c1773d48c83e 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 @@ -1,62 +1,121 @@ package org.wordpress.android.ui.sitemonitor import android.annotation.SuppressLint +import android.os.Build import android.os.Bundle import android.util.SparseArray +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.foundation.lazy.LazyColumn +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Surface +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable +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.text.style.TextOverflow +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.util.extensions.getSerializableExtraCompat import javax.inject.Inject +@SuppressLint("SetJavaScriptEnabled") @AndroidEntryPoint -class SiteMonitorParentActivity: AppCompatActivity() { +class SiteMonitorParentActivity : AppCompatActivity(), SiteMonitorWebViewClient.SiteMonitorWebViewClientListener { @Inject lateinit var siteMonitorUtils: SiteMonitorUtils private var savedStateSparseArray = SparseArray() private var currentSelectItemId = 0 + private val siteMonitorParentViewModel: SiteMonitorParentViewModel by viewModels() + + private val metricsWebView by lazy { + commonWebView(SiteMonitorType.METRICS) + } + + private val phpLogsWebView by lazy { + commonWebView(SiteMonitorType.PHP_LOGS) + } + + private val webServerLogsWebView by lazy { + commonWebView(SiteMonitorType.WEB_SERVER_LOGS) + } + + private fun commonWebView( + siteMonitorType: SiteMonitorType + ) = WebView(this@SiteMonitorParentActivity).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY + settings.userAgentString = siteMonitorUtils.getUserAgent() + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + webViewClient = SiteMonitorWebViewClient(this@SiteMonitorParentActivity, siteMonitorType) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // not sure about this one, double check if this works as expected + settings.isAlgorithmicDarkeningAllowed = true + } + } + @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - siteMonitorUtils.trackActivityLaunched() - if (savedInstanceState != null) { savedStateSparseArray = savedInstanceState.getSparseParcelableArray( SAVED_STATE_CONTAINER_KEY ) ?: savedStateSparseArray currentSelectItemId = savedInstanceState.getInt(SAVED_STATE_CURRENT_TAB_KEY) + siteMonitorParentViewModel.loadData() + } else { + siteMonitorParentViewModel.start(getSite()) + currentSelectItemId = getInitialTab() } setContent { AppTheme { Surface( modifier = Modifier.fillMaxSize(), ) { - SiteMonitorScreen() + SiteMonitorScreen(initialTab = currentSelectItemId) } } } @@ -72,9 +131,14 @@ class SiteMonitorParentActivity: AppCompatActivity() { return requireNotNull(intent.getSerializableExtraCompat(WordPress.SITE)) as SiteModel } - private fun getInitialTab(): SiteMonitorType { - return intent?.getSerializableExtraCompat(ARG_SITE_MONITOR_TYPE_KEY) as SiteMonitorType? + private fun getInitialTab(): Int { + val tab = intent?.getSerializableExtraCompat(ARG_SITE_MONITOR_TYPE_KEY) as SiteMonitorType? ?: SiteMonitorType.METRICS + return when (tab) { + SiteMonitorType.METRICS -> 0 + SiteMonitorType.PHP_LOGS -> 1 + SiteMonitorType.WEB_SERVER_LOGS -> 2 + } } companion object { @@ -85,8 +149,7 @@ class SiteMonitorParentActivity: AppCompatActivity() { @Composable @SuppressLint("UnusedMaterialScaffoldPaddingParameter") - fun SiteMonitorScreen() { - var selectedTab by rememberSaveable { mutableStateOf(SiteMonitorTabItem.Metrics.route) } + fun SiteMonitorScreen(initialTab: Int, modifier: Modifier = Modifier) { Scaffold( topBar = { MainTopAppBar( @@ -94,59 +157,171 @@ class SiteMonitorParentActivity: AppCompatActivity() { navigationIcon = NavigationIcons.BackIcon, onNavigationIconClick = onBackPressedDispatcher::onBackPressed, ) + }, + content = { + SiteMonitorHeader(initialTab, modifier = modifier) } - ) { padding -> - Column(modifier = Modifier.padding(padding)) { - SiteMonitorTabHeader { clickTab -> - selectedTab = clickTab + ) + } + + @Composable + @SuppressLint("UnusedMaterialScaffoldPaddingParameter") + fun SiteMonitorHeader(initialTab: Int, modifier: Modifier = Modifier) { + var tabIndex by remember { mutableStateOf(initialTab) } + + val tabs = SiteMonitorTabItem.entries + + LaunchedEffect(true) { + siteMonitorUtils.trackTabLoaded(tabs[initialTab].siteMonitorType) + } + + Column(modifier = modifier.fillMaxWidth()) { + TabRow( + selectedTabIndex = tabIndex, + containerColor = MaterialTheme.colors.surface, + contentColor = MaterialTheme.colors.onSurface, + indicator = { tabPositions -> + // Customizing the indicator color and style + TabRowDefaults.Indicator( + Modifier.tabIndicatorOffset(tabPositions[tabIndex]), + color = MaterialTheme.colors.onSurface, + height = 2.0.dp + ) } - SiteMonitorTabNavigation(selectedTab) { selectedTab -> - val item = enumValues().find { - it.route == selectedTab - } ?: initialItem(getInitialTab()) - - siteMonitorUtils.trackTabLoaded(item.siteMonitorType) - - SiteMonitorFragmentContainer( - modifier = Modifier.fillMaxSize(), - commit = getCommitFunction( - SiteMonitorTabFragment.newInstance(item.urlTemplate, item.siteMonitorType, getSite()), - item.route - ) + ) { + tabs.forEachIndexed { index, item -> + Tab( + text = { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = stringResource(item.title).uppercase(), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + }, + selected = tabIndex == index, + onClick = { + siteMonitorUtils.trackTabLoaded(tabs[index].siteMonitorType) + tabIndex = index + }, ) } } + when (tabIndex) { + 0 -> SiteMonitorTabContent(SiteMonitorType.METRICS) + 1 -> SiteMonitorTabContent(SiteMonitorType.PHP_LOGS) + 2 -> SiteMonitorTabContent(SiteMonitorType.WEB_SERVER_LOGS) + } } } - private fun initialItem(type: SiteMonitorType): SiteMonitorTabItem { - return enumValues().find { - it.siteMonitorType == type - } ?: SiteMonitorTabItem.Metrics + @Composable + private fun SiteMonitorTabContent(tabType: SiteMonitorType, modifier: Modifier = Modifier) { + val uiState by remember(key1 = tabType) { + siteMonitorParentViewModel.getUiState(tabType) + } + LazyColumn { + item { + when (uiState) { + is SiteMonitorUiState.Preparing -> LoadingState(modifier) + is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded -> + SiteMonitorWebView(uiState, tabType, modifier) + is SiteMonitorUiState.Error -> SiteMonitorError(uiState as SiteMonitorUiState.Error, modifier) + } + } + } + } + @Composable + fun LoadingState(modifier: Modifier = Modifier) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier.fillMaxSize() + ) { + CircularProgressIndicator() + } } - private fun getCommitFunction( - fragment : Fragment, - tag: String - ): FragmentTransaction.(containerId: Int) -> Unit = - { - saveAndRetrieveFragment(supportFragmentManager, it, fragment) - replace(it, fragment, tag) + @Composable + fun SiteMonitorError(error: SiteMonitorUiState.Error, modifier: Modifier = Modifier) { + 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 + ) { + Text(text = uiStringText(uiString = error.button.text)) + } + } } + } - private fun saveAndRetrieveFragment( - supportFragmentManager: FragmentManager, - tabId: Int, - fragment: Fragment + @SuppressLint("SetJavaScriptEnabled") + @Composable + private fun SiteMonitorWebView( + uiState: SiteMonitorUiState, + tabType: SiteMonitorType, + modifier: Modifier = Modifier ) { - val currentFragment = supportFragmentManager.findFragmentById(currentSelectItemId) - if (currentFragment != null) { - savedStateSparseArray.put( - currentSelectItemId, - supportFragmentManager.saveFragmentInstanceState(currentFragment) - ) + // retrieve the webview from the actvity + var webView = when (tabType) { + SiteMonitorType.METRICS -> metricsWebView + SiteMonitorType.PHP_LOGS -> phpLogsWebView + SiteMonitorType.WEB_SERVER_LOGS -> webServerLogsWebView } - currentSelectItemId = tabId - fragment.setInitialSavedState(savedStateSparseArray[currentSelectItemId]) + + if (uiState is SiteMonitorUiState.Prepared) { + webView.postUrl(WPWebViewActivity.WPCOM_LOGIN_URL, uiState.model.addressToLoad.toByteArray()) + } + + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + if (uiState is SiteMonitorUiState.Prepared) { + LoadingState() + } else { + webView.let { theWebView -> + AndroidView( + factory = { theWebView }, + update = { webView = it }, + modifier = Modifier.fillMaxWidth() + ) + } + } + } + } + + override fun onDestroy() { + super.onDestroy() + metricsWebView.destroy() + phpLogsWebView.destroy() + webServerLogsWebView.destroy() + } + + override fun onWebViewPageLoaded(url: String, tabType: SiteMonitorType) = + siteMonitorParentViewModel.onUrlLoaded(tabType) + + override fun onWebViewReceivedError(url: String, tabType: SiteMonitorType) { + siteMonitorParentViewModel.onWebViewError(tabType) + siteMonitorUtils.trackTabLoadingError(tabType) } } 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 new file mode 100644 index 000000000000..2a18f07e1fb2 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt @@ -0,0 +1,95 @@ +package org.wordpress.android.ui.sitemonitor + +import androidx.compose.runtime.State +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.modules.BG_THREAD +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 siteMonitorUtils: SiteMonitorUtils, + private val metricsViewModel: SiteMonitorTabViewModelSlice, + private val phpLogViewModel: SiteMonitorTabViewModelSlice, + private val webServerViewModel: SiteMonitorTabViewModelSlice +) : ScopedViewModel(bgDispatcher) { + private lateinit var site: SiteModel + + init { + metricsViewModel.initialize(viewModelScope) + phpLogViewModel.initialize(viewModelScope) + webServerViewModel.initialize(viewModelScope) + } + + fun start(site: SiteModel) { + this.site = site + siteMonitorUtils.trackActivityLaunched() + loadData() + } + + fun loadData() { + metricsViewModel.start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site) + phpLogViewModel.start(SiteMonitorType.PHP_LOGS, SiteMonitorTabItem.PHPLogs.urlTemplate, site) + webServerViewModel.start(SiteMonitorType.WEB_SERVER_LOGS, SiteMonitorTabItem.WebServerLogs.urlTemplate, site) + } + + fun getUiState(siteMonitorType: SiteMonitorType): State { + return when (siteMonitorType) { + SiteMonitorType.METRICS -> { + metricsViewModel.uiState + } + + SiteMonitorType.PHP_LOGS -> { + phpLogViewModel.uiState + } + + SiteMonitorType.WEB_SERVER_LOGS -> { + webServerViewModel.uiState + } + } + } + + fun onUrlLoaded(siteMonitorType: SiteMonitorType) { + when (siteMonitorType) { + SiteMonitorType.METRICS -> { + metricsViewModel.onUrlLoaded() + } + + SiteMonitorType.PHP_LOGS -> { + phpLogViewModel.onUrlLoaded() + } + + SiteMonitorType.WEB_SERVER_LOGS -> { + webServerViewModel.onUrlLoaded() + } + } + } + + fun onWebViewError(siteMonitorType: SiteMonitorType) { + when (siteMonitorType) { + SiteMonitorType.METRICS -> { + metricsViewModel.onWebViewError() + } + + SiteMonitorType.PHP_LOGS -> { + phpLogViewModel.onWebViewError() + } + + SiteMonitorType.WEB_SERVER_LOGS -> { + webServerViewModel.onWebViewError() + } + } + } + + override fun onCleared() { + super.onCleared() + metricsViewModel.onCleared() + phpLogViewModel.onCleared() + webServerViewModel.onCleared() + } +} 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 deleted file mode 100644 index 8fb61cc8ed03..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabFragment.kt +++ /dev/null @@ -1,184 +0,0 @@ -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 { - SiteMonitorTabContent() - } - } - - 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 SiteMonitorTabContent() { - val uiState by viewModel.uiState.collectAsState() - when (uiState) { - is SiteMonitorUiState.Preparing -> LoadingState() - is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded -> SiteMonitorWebView(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 SiteMonitorWebView(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/SiteMonitorTabHeader.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabHeader.kt deleted file mode 100644 index 7a15059a31fc..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabHeader.kt +++ /dev/null @@ -1,61 +0,0 @@ -package org.wordpress.android.ui.sitemonitor - -import androidx.compose.foundation.layout.Column -import androidx.compose.material.MaterialTheme -import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow -import androidx.compose.material3.TabRowDefaults -import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset -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.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp - -@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.colors.surface, - contentColor = MaterialTheme.colors.onSurface, - indicator = { tabPositions -> - // Customizing the indicator color and style - TabRowDefaults.Indicator( - Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]), - color = MaterialTheme.colors.onSurface, - height = 2.0.dp - ) - } - ) { - tabs.forEachIndexed { index, item -> - Tab( - text = { - Column (horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = stringResource(item.title), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - }, - selected = selectedTabIndex == index, - onClick = { - selectedTabIndex = index - navController(item.route) - }, - ) - } - } -} 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 deleted file mode 100644 index 023e651a66d5..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabNavigation.kt +++ /dev/null @@ -1,20 +0,0 @@ -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) - } - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSlice.kt similarity index 77% rename from WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModel.kt rename to WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSlice.kt index 275c65087676..45ec9a56d76f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSlice.kt @@ -1,34 +1,36 @@ 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 androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch 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, +class SiteMonitorTabViewModelSlice @Inject constructor( 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 scope: CoroutineScope + 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 + private val _uiState = mutableStateOf(SiteMonitorUiState.Preparing) + val uiState: State = _uiState + + fun initialize(scope: CoroutineScope) { + this.scope = scope + } fun start(type: SiteMonitorType, urlTemplate: String, site: SiteModel) { this.siteMonitorType = type @@ -50,13 +52,13 @@ class SiteMonitorTabViewModel @Inject constructor( private fun checkForInternetConnectivityAndPostErrorIfNeeded() : Boolean { if (networkUtilsWrapper.isNetworkAvailable()) return true - postUiState(mapper.toNoNetworkError(this@SiteMonitorTabViewModel::loadView)) + postUiState(mapper.toNoNetworkError(::loadView)) return false } private fun validateAndPostErrorIfNeeded(): Boolean { if (accountStore.account.userName.isNullOrEmpty() || accountStore.accessToken.isNullOrEmpty()) { - postUiState(mapper.toGenericError(this@SiteMonitorTabViewModel::loadView)) + postUiState(mapper.toGenericError(this::loadView)) return false } return true @@ -98,17 +100,23 @@ class SiteMonitorTabViewModel @Inject constructor( } private fun postUiState(state: SiteMonitorUiState) { - launch { + scope.launch { _uiState.value = state } } fun onUrlLoaded() { - postUiState(SiteMonitorUiState.Loaded) + if (uiState.value is SiteMonitorUiState.Prepared){ + postUiState(SiteMonitorUiState.Loaded) + } } fun onWebViewError() { - postUiState(mapper.toGenericError(this@SiteMonitorTabViewModel::loadView)) + postUiState(mapper.toGenericError(::loadView)) + } + + fun onCleared() { + scope.cancel() } companion object { 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 a4b25ed2a1c7..a64e92029030 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 @@ -33,6 +33,14 @@ class SiteMonitorUtils @Inject constructor( )) } + fun trackTabLoadingError(siteMonitorType: SiteMonitorType) { + analyticsTrackerWrapper.track( + AnalyticsTracker.Stat.SITE_MONITORING_TAB_LOADING_ERROR, + mapOf( + TAB_TRACK_KEY to siteMonitorType.analyticsDescription + )) + } + companion object { const val HTTP_PATTERN = "(https?://)" const val PHP_LOGS_PATTERN = "/php" 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 29305e0d2ea0..af0717e8481b 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 @@ -7,13 +7,15 @@ import android.webkit.WebView import android.webkit.WebViewClient class SiteMonitorWebViewClient( - private val listener: SiteMonitorWebViewClientListener + private val listener: SiteMonitorWebViewClientListener, + private val tabType: SiteMonitorType ) : WebViewClient() { private var errorReceived = false private var requestedUrl: String? = null + interface SiteMonitorWebViewClientListener { - fun onWebViewPageLoaded(url: String) - fun onWebViewReceivedError(url: String) + fun onWebViewPageLoaded(url: String, tabType: SiteMonitorType) + fun onWebViewReceivedError(url: String, tabType: SiteMonitorType) } override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { return false @@ -28,7 +30,7 @@ class SiteMonitorWebViewClient( override fun onPageFinished(view: WebView, url: String?) { super.onPageFinished(view, url) if (!errorReceived) { - url?.let { listener.onWebViewPageLoaded(it) } + url?.let { listener.onWebViewPageLoaded(it, tabType) } } } @@ -40,7 +42,7 @@ class SiteMonitorWebViewClient( // > 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()) + listener.onWebViewReceivedError(request.url.toString(), tabType) } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsModule.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsModule.kt index a3cbd2848638..74984c278010 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsModule.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsModule.kt @@ -67,6 +67,7 @@ import javax.inject.Named import javax.inject.Singleton const val INSIGHTS_USE_CASE = "InsightsUseCase" +const val TRAFFIC_USE_CASE = "TrafficStatsUseCase" const val DAY_STATS_USE_CASE = "DayStatsUseCase" const val WEEK_STATS_USE_CASE = "WeekStatsUseCase" const val MONTH_STATS_USE_CASE = "MonthStatsUseCase" @@ -265,6 +266,32 @@ class StatsModule { ) } + /** + * Provides a singleton usecase that represents the TRAFFIC stats screen. + * @param useCasesFactories build the use cases for the DAYS granularity + */ + @Provides + @Singleton + @Named(TRAFFIC_USE_CASE) + @Suppress("LongParameterList") + fun provideTrafficUseCase( + statsStore: StatsStore, + @Named(BG_THREAD) bgDispatcher: CoroutineDispatcher, + @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, + statsSiteProvider: StatsSiteProvider, + @Named(GRANULAR_USE_CASE_FACTORIES) useCasesFactories: List<@JvmSuppressWildcards GranularUseCaseFactory>, + uiModelMapper: UiModelMapper + ): BaseListUseCase { + return BaseListUseCase( + bgDispatcher, + mainDispatcher, + statsSiteProvider, + useCasesFactories.map { it.build(DAYS, BLOCK) }, + { statsStore.getTimeStatsTypes(it) }, + uiModelMapper::mapTimeStats + ) + } + /** * Provides a singleton usecase that represents the Day stats screen. * @param useCasesFactories build the use cases for the DAYS granularity @@ -374,8 +401,10 @@ class StatsModule { @Provides @Singleton @Named(LIST_STATS_USE_CASES) + @Suppress("LongParameterList") fun provideListStatsUseCases( @Named(INSIGHTS_USE_CASE) insightsUseCase: BaseListUseCase, + @Named(TRAFFIC_USE_CASE) trafficUseCase: BaseListUseCase, @Named(DAY_STATS_USE_CASE) dayStatsUseCase: BaseListUseCase, @Named(WEEK_STATS_USE_CASE) weekStatsUseCase: BaseListUseCase, @Named(MONTH_STATS_USE_CASE) monthStatsUseCase: BaseListUseCase, @@ -383,6 +412,7 @@ class StatsModule { ): Map { return mapOf( StatsSection.INSIGHTS to insightsUseCase, + StatsSection.TRAFFIC to trafficUseCase, StatsSection.DAYS to dayStatsUseCase, StatsSection.WEEKS to weekStatsUseCase, StatsSection.MONTHS to monthStatsUseCase, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllFragment.kt index 952113eb5ee0..80d8688c73a7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllFragment.kt @@ -191,17 +191,17 @@ class StatsViewAllFragment : Fragment(R.layout.stats_view_all_fragment) { } private fun StatsViewAllFragmentBinding.setupObservers(activity: FragmentActivity) { - viewModel.isRefreshing.observe(viewLifecycleOwner, { + viewModel.isRefreshing.observe(viewLifecycleOwner) { it?.let { isRefreshing -> swipeToRefreshHelper.isRefreshing = isRefreshing } - }) + } - viewModel.showSnackbarMessage.observeEvent(viewLifecycleOwner, { holder -> + viewModel.showSnackbarMessage.observeEvent(viewLifecycleOwner) { holder -> showSnackbar(activity, holder) - }) + } - viewModel.data.observe(viewLifecycleOwner, { + viewModel.data.observe(viewLifecycleOwner) { if (it != null) { with(statsListFragment) { recyclerView.visibility = if (it is Success) View.VISIBLE else View.GONE @@ -214,41 +214,44 @@ class StatsViewAllFragment : Fragment(R.layout.stats_view_all_fragment) { is Success -> { loadData(recyclerView, prepareLayout(it.data, it.type)) } + is Loading -> { loadData(loadingRecyclerView, prepareLayout(it.data, it.type)) } + is Error -> { errorView.statsErrorView.button.setOnClickListener { viewModel.onRetryClick() } } + is EmptyBlock -> { } } } } - }) - viewModel.navigationTarget.observeEvent(viewLifecycleOwner, { target -> + } + viewModel.navigationTarget.observeEvent(viewLifecycleOwner) { target -> navigator.navigate(activity, target) - }) + } - viewModel.dateSelectorData.observe(viewLifecycleOwner, { dateSelectorUiModel -> + viewModel.dateSelectorData.observe(viewLifecycleOwner) { dateSelectorUiModel -> statsListFragment.drawDateSelector(dateSelectorUiModel) - }) + } - viewModel.navigationTarget.observeEvent(viewLifecycleOwner, { target -> + viewModel.navigationTarget.observeEvent(viewLifecycleOwner) { target -> navigator.navigate(activity, target) - }) + } - viewModel.selectedDate.observe(viewLifecycleOwner, { event -> + viewModel.selectedDate?.observe(viewLifecycleOwner) { event -> if (event != null) { viewModel.onDateChanged() } - }) + } - viewModel.toolbarHasShadow.observe(viewLifecycleOwner, { hasShadow -> + viewModel.toolbarHasShadow.observe(viewLifecycleOwner) { hasShadow -> appBarLayout.showShadow(hasShadow == true) - }) + } } private fun showSnackbar( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllViewModel.kt index 57785b26a6be..73de971c26d3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllViewModel.kt @@ -20,8 +20,8 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.granular.SelectedDa import org.wordpress.android.ui.stats.refresh.utils.StatsDateSelector import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider import org.wordpress.android.ui.utils.UiString.UiStringRes -import org.wordpress.android.util.mapSafe import org.wordpress.android.util.mapNullable +import org.wordpress.android.util.mapSafe import org.wordpress.android.util.throttle import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel @@ -31,14 +31,14 @@ class StatsViewAllViewModel( val bgDispatcher: CoroutineDispatcher, val useCase: BaseStatsUseCase<*, *>, private val statsSiteProvider: StatsSiteProvider, - private val dateSelector: StatsDateSelector, + private val dateSelector: StatsDateSelector?, @StringRes val title: Int ) : ScopedViewModel(mainDispatcher) { - val selectedDate = dateSelector.selectedDate + val selectedDate = dateSelector?.selectedDate - val dateSelectorData: LiveData = dateSelector.dateSelectorData.mapNullable { + val dateSelectorData: LiveData = dateSelector?.dateSelectorData?.mapNullable { it ?: DateSelectorUiModel(false) - } + } ?: MutableLiveData(DateSelectorUiModel(false)) val navigationTarget: LiveData> = useCase.navigationTarget @@ -62,12 +62,12 @@ class StatsViewAllViewModel( fun start(startDate: SelectedDate?) { launch { startDate?.let { - dateSelector.start(startDate) + dateSelector?.start(startDate) } loadData(refresh = false, forced = false) - dateSelector.updateDateSelector() + dateSelector?.updateDateSelector() } - dateSelector.updateDateSelector() + dateSelector?.updateDateSelector() } @SuppressLint("NullSafeMutableLiveData") @@ -109,13 +109,13 @@ class StatsViewAllViewModel( fun onNextDateSelected() { launch(mainDispatcher) { - dateSelector.onNextDateSelected() + dateSelector?.onNextDateSelected() } } fun onPreviousDateSelected() { launch(mainDispatcher) { - dateSelector.onPreviousDateSelected() + dateSelector?.onPreviousDateSelected() } } @@ -131,7 +131,7 @@ class StatsViewAllViewModel( } } - fun getSelectedDate(): SelectedDate { - return dateSelector.getSelectedDate() + fun getSelectedDate(): SelectedDate? { + return dateSelector?.getSelectedDate() } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllViewModelFactory.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllViewModelFactory.kt index 167735228b8c..cec09374d5c0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllViewModelFactory.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllViewModelFactory.kt @@ -29,8 +29,6 @@ import org.wordpress.android.ui.stats.StatsViewType.SEARCH_TERMS import org.wordpress.android.ui.stats.StatsViewType.TAGS_AND_CATEGORIES import org.wordpress.android.ui.stats.StatsViewType.TOP_POSTS_AND_PAGES import org.wordpress.android.ui.stats.StatsViewType.VIDEO_PLAYS -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.INSIGHTS import org.wordpress.android.ui.stats.refresh.lists.detail.PostAverageViewsPerDayUseCase import org.wordpress.android.ui.stats.refresh.lists.detail.PostMonthsAndYearsUseCase import org.wordpress.android.ui.stats.refresh.lists.detail.PostRecentWeeksUseCase @@ -56,7 +54,6 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.insights.usecases.T import org.wordpress.android.ui.stats.refresh.lists.sections.insights.usecases.ViewsAndVisitorsUseCase import org.wordpress.android.ui.stats.refresh.utils.StatsDateSelector import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider -import org.wordpress.android.ui.stats.refresh.utils.toStatsSection import java.security.InvalidParameterException import javax.inject.Inject import javax.inject.Named @@ -66,7 +63,7 @@ class StatsViewAllViewModelFactory( private val bgDispatcher: CoroutineDispatcher, private val useCase: BaseStatsUseCase<*, *>, private val statsSiteProvider: StatsSiteProvider, - private val dateSelector: StatsDateSelector, + private val dateSelector: StatsDateSelector?, @StringRes private val titleResource: Int ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @@ -92,34 +89,29 @@ class StatsViewAllViewModelFactory( private val statsSiteProvider: StatsSiteProvider, private val dateSelectorFactory: StatsDateSelector.Factory ) { - fun build(type: StatsViewType, granularity: StatsGranularity?): StatsViewAllViewModelFactory { - return when { - type == ANNUAL_STATS -> buildAnnualStatsFactory() - granularity == null -> buildFactory(type) - else -> buildFactory(type, granularity) - } - } - - private fun buildFactory(type: StatsViewType, granularity: StatsGranularity): StatsViewAllViewModelFactory { - val (useCase, title) = getGranularUseCase(type, granularity, granularFactories) - return StatsViewAllViewModelFactory( - mainDispatcher, - bgDispatcher, - useCase, - statsSiteProvider, - dateSelectorFactory.build(granularity.toStatsSection()), - title - ) + fun build(type: StatsViewType, granularity: StatsGranularity?) = if (type == ANNUAL_STATS) { + buildAnnualStatsFactory() + } else { + buildFactory(type, granularity) } - private fun buildFactory(type: StatsViewType): StatsViewAllViewModelFactory { - val (useCase, title) = getInsightsUseCase(type, insightsUseCases) + private fun buildFactory(type: StatsViewType, granularity: StatsGranularity?): StatsViewAllViewModelFactory { + val (useCase, title) = if (granularity == null) { + getInsightsUseCase(type, insightsUseCases) + } else { + getGranularUseCase(type, granularity, granularFactories) + } + val dateSelector = if (granularity == null) { + null + } else { + dateSelectorFactory.build(granularity) + } return StatsViewAllViewModelFactory( mainDispatcher, bgDispatcher, useCase, statsSiteProvider, - dateSelectorFactory.build(INSIGHTS), + dateSelector, title ) } @@ -133,7 +125,7 @@ class StatsViewAllViewModelFactory( bgDispatcher, useCase, statsSiteProvider, - dateSelectorFactory.build(StatsSection.ANNUAL_STATS), + dateSelectorFactory.build(StatsGranularity.YEARS), R.string.stats_insights_annual_site_stats ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/BaseListUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/BaseListUseCase.kt index 6c8632bbb5f3..b01c21f2abc7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/BaseListUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/BaseListUseCase.kt @@ -9,10 +9,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.wordpress.android.R import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.network.utils.StatsGranularity import org.wordpress.android.fluxc.store.StatsStore.StatsType import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.stats.refresh.NavigationTarget -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.UiModel import org.wordpress.android.ui.stats.refresh.lists.sections.BaseStatsUseCase import org.wordpress.android.ui.stats.refresh.lists.sections.BaseStatsUseCase.UseCaseModel @@ -140,8 +140,8 @@ class BaseListUseCase( data.value = null } - suspend fun onDateChanged(selectedSection: StatsSection) { - onParamChanged(UseCaseParam.SelectedDateParam(selectedSection)) + suspend fun onDateChanged(selectedGranularity: StatsGranularity) { + onParamChanged(UseCaseParam.SelectedDateParam(selectedGranularity)) } fun onListSelected() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListFragment.kt index 8169b738f924..1469325b5c26 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListFragment.kt @@ -27,6 +27,7 @@ import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.UiModel.E import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.UiModel.Error import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.UiModel.Success import org.wordpress.android.ui.stats.refresh.lists.detail.DetailListViewModel +import org.wordpress.android.ui.stats.refresh.utils.SelectedTrafficGranularityManager import org.wordpress.android.ui.stats.refresh.utils.StatsDateFormatter import org.wordpress.android.ui.stats.refresh.utils.StatsNavigator import org.wordpress.android.ui.stats.refresh.utils.drawDateSelector @@ -57,6 +58,9 @@ class StatsListFragment : ViewPagerFragment(R.layout.stats_list_fragment) { @Inject lateinit var statsTrafficTabFeatureConfig: StatsTrafficTabFeatureConfig + @Inject + lateinit var selectedTrafficGranularityManager: SelectedTrafficGranularityManager + private lateinit var viewModel: StatsListViewModel private lateinit var statsSection: StatsSection @@ -158,9 +162,14 @@ class StatsListFragment : ViewPagerFragment(R.layout.stats_list_fragment) { StatsGranularity.entries.map { getString(it.toNameResource()) } ).apply { setDropDownViewResource(R.layout.toolbar_spinner_dropdown_item) } + val selectedGranularityItemPos = StatsGranularity.entries.indexOf( + selectedTrafficGranularityManager.getSelectedTrafficGranularity() + ) + dateSelector.granularitySpinner.setSelection(selectedGranularityItemPos) + dateSelector.granularitySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - // TODO update TRAFFIC tab + selectedTrafficGranularityManager.setSelectedTrafficGranularity(StatsGranularity.entries[position]) } @Suppress("EmptyFunctionBlock") @@ -243,9 +252,9 @@ class StatsListFragment : ViewPagerFragment(R.layout.stats_list_fragment) { navigator.navigate(activity, target) } - viewModel.selectedDate.observe(viewLifecycleOwner) { event -> + viewModel.selectedDate?.observe(viewLifecycleOwner) { event -> if (event != null) { - viewModel.onDateChanged(event.selectedSection) + viewModel.onDateChanged(event.selectedGranularity) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListViewModel.kt index 386a6575a3a7..1643bedabaae 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListViewModel.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker.Stat +import org.wordpress.android.fluxc.network.utils.StatsGranularity import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.stats.refresh.DAY_STATS_USE_CASE import org.wordpress.android.ui.stats.refresh.INSIGHTS_USE_CASE @@ -20,15 +21,10 @@ import org.wordpress.android.ui.stats.refresh.StatsViewModel.DateSelectorUiModel import org.wordpress.android.ui.stats.refresh.TOTAL_COMMENTS_DETAIL_USE_CASE import org.wordpress.android.ui.stats.refresh.TOTAL_FOLLOWERS_DETAIL_USE_CASE import org.wordpress.android.ui.stats.refresh.TOTAL_LIKES_DETAIL_USE_CASE +import org.wordpress.android.ui.stats.refresh.TRAFFIC_USE_CASE import org.wordpress.android.ui.stats.refresh.VIEWS_AND_VISITORS_USE_CASE import org.wordpress.android.ui.stats.refresh.WEEK_STATS_USE_CASE import org.wordpress.android.ui.stats.refresh.YEAR_STATS_USE_CASE -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.DAYS -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.INSIGHTS -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.MONTHS -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.TRAFFIC -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.WEEKS -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.YEARS import org.wordpress.android.ui.stats.refresh.utils.ActionCardHandler import org.wordpress.android.ui.stats.refresh.utils.ItemPopupMenuHandler import org.wordpress.android.ui.stats.refresh.utils.NewsCardHandler @@ -49,7 +45,7 @@ abstract class StatsListViewModel( defaultDispatcher: CoroutineDispatcher, private val statsUseCase: BaseListUseCase, private val analyticsTracker: AnalyticsTrackerWrapper, - protected val dateSelector: StatsDateSelector, + protected val dateSelector: StatsDateSelector?, popupMenuHandler: ItemPopupMenuHandler? = null, private val newsCardHandler: NewsCardHandler? = null, actionCardHandler: ActionCardHandler? = null @@ -72,7 +68,7 @@ abstract class StatsListViewModel( ANNUAL_STATS(R.string.stats_insights_annual_site_stats); } - val selectedDate = dateSelector.selectedDate + val selectedDate = dateSelector?.selectedDate private val mutableNavigationTarget = MutableLiveData>() val navigationTarget: LiveData> = mergeNotNull( @@ -85,9 +81,9 @@ abstract class StatsListViewModel( statsUseCase.data.throttle(viewModelScope, distinct = true) } - val dateSelectorData: LiveData = dateSelector.dateSelectorData.mapNullable { + val dateSelectorData: LiveData = dateSelector?.dateSelectorData?.mapNullable { it ?: DateSelectorUiModel(false) - } + } ?: MutableLiveData(DateSelectorUiModel(false)) val typesChanged = merge( popupMenuHandler?.typeMoved, @@ -115,13 +111,13 @@ abstract class StatsListViewModel( fun onNextDateSelected() { launch(Dispatchers.Default) { - dateSelector.onNextDateSelected() + dateSelector?.onNextDateSelected() } } fun onPreviousDateSelected() { launch(Dispatchers.Default) { - dateSelector.onPreviousDateSelected() + dateSelector?.onPreviousDateSelected() } } @@ -131,14 +127,14 @@ abstract class StatsListViewModel( } } - fun onDateChanged(selectedSection: StatsSection) { + fun onDateChanged(selectedGranularity: StatsGranularity) { launch { - statsUseCase.onDateChanged(selectedSection) + statsUseCase.onDateChanged(selectedGranularity) } } fun onListSelected() { - dateSelector.updateDateSelector() + dateSelector?.updateDateSelector() } fun onEmptyInsightsButtonClicked() { @@ -156,10 +152,10 @@ abstract class StatsListViewModel( isInitialized = true launch { statsUseCase.loadData() - dateSelector.updateDateSelector() + dateSelector?.updateDateSelector() } } - dateSelector.updateDateSelector() + dateSelector?.updateDateSelector() } sealed class UiModel { @@ -185,7 +181,6 @@ class InsightsListViewModel @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, @Named(INSIGHTS_USE_CASE) private val insightsUseCase: BaseListUseCase, analyticsTracker: AnalyticsTrackerWrapper, - dateSelectorFactory: StatsDateSelector.Factory, popupMenuHandler: ItemPopupMenuHandler, newsCardHandler: NewsCardHandler, actionCardHandler: ActionCardHandler @@ -193,72 +188,117 @@ class InsightsListViewModel mainDispatcher, insightsUseCase, analyticsTracker, - dateSelectorFactory.build(INSIGHTS), + null, popupMenuHandler, newsCardHandler, actionCardHandler ) +class TrafficListViewModel @Inject constructor( + @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, + @Named(TRAFFIC_USE_CASE) statsUseCase: BaseListUseCase, + analyticsTracker: AnalyticsTrackerWrapper, + dateSelectorFactory: StatsDateSelector.Factory +) : StatsListViewModel( + mainDispatcher, + statsUseCase, + analyticsTracker, + dateSelectorFactory.build(StatsGranularity.DAYS, isGranularitySpinnerVisible = true) +) + class YearsListViewModel @Inject constructor( @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, @Named(YEAR_STATS_USE_CASE) statsUseCase: BaseListUseCase, analyticsTracker: AnalyticsTrackerWrapper, dateSelectorFactory: StatsDateSelector.Factory -) : StatsListViewModel(mainDispatcher, statsUseCase, analyticsTracker, dateSelectorFactory.build(YEARS)) +) : StatsListViewModel( + mainDispatcher, + statsUseCase, + analyticsTracker, + dateSelectorFactory.build(StatsGranularity.YEARS) +) class MonthsListViewModel @Inject constructor( @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, @Named(MONTH_STATS_USE_CASE) statsUseCase: BaseListUseCase, analyticsTracker: AnalyticsTrackerWrapper, dateSelectorFactory: StatsDateSelector.Factory -) : StatsListViewModel(mainDispatcher, statsUseCase, analyticsTracker, dateSelectorFactory.build(MONTHS)) +) : StatsListViewModel( + mainDispatcher, + statsUseCase, + analyticsTracker, + dateSelectorFactory.build(StatsGranularity.MONTHS) +) class WeeksListViewModel @Inject constructor( @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, @Named(WEEK_STATS_USE_CASE) statsUseCase: BaseListUseCase, analyticsTracker: AnalyticsTrackerWrapper, dateSelectorFactory: StatsDateSelector.Factory -) : StatsListViewModel(mainDispatcher, statsUseCase, analyticsTracker, dateSelectorFactory.build(WEEKS)) +) : StatsListViewModel( + mainDispatcher, + statsUseCase, + analyticsTracker, + dateSelectorFactory.build(StatsGranularity.WEEKS) +) class DaysListViewModel @Inject constructor( @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, @Named(DAY_STATS_USE_CASE) statsUseCase: BaseListUseCase, analyticsTracker: AnalyticsTrackerWrapper, dateSelectorFactory: StatsDateSelector.Factory -) : StatsListViewModel(mainDispatcher, statsUseCase, analyticsTracker, dateSelectorFactory.build(DAYS)) - -class TrafficListViewModel @Inject constructor( - @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, - @Named(DAY_STATS_USE_CASE) statsUseCase: BaseListUseCase, - analyticsTracker: AnalyticsTrackerWrapper, - dateSelectorFactory: StatsDateSelector.Factory -) : StatsListViewModel(mainDispatcher, statsUseCase, analyticsTracker, dateSelectorFactory.build(TRAFFIC)) +) : StatsListViewModel( + mainDispatcher, + statsUseCase, + analyticsTracker, + dateSelectorFactory.build(StatsGranularity.DAYS) +) -// Using Weeks granularity on new insight details screens +// Using Weeks granularity on insight details screens class InsightsDetailListViewModel @Inject constructor( @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, @Named(VIEWS_AND_VISITORS_USE_CASE) statsUseCase: BaseListUseCase, analyticsTracker: AnalyticsTrackerWrapper, dateSelectorFactory: StatsDateSelector.Factory -) : StatsListViewModel(mainDispatcher, statsUseCase, analyticsTracker, dateSelectorFactory.build(WEEKS)) +) : StatsListViewModel( + mainDispatcher, + statsUseCase, + analyticsTracker, + dateSelectorFactory.build(StatsGranularity.WEEKS) +) class TotalLikesDetailListViewModel @Inject constructor( @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, @Named(TOTAL_LIKES_DETAIL_USE_CASE) statsUseCase: BaseListUseCase, analyticsTracker: AnalyticsTrackerWrapper, dateSelectorFactory: StatsDateSelector.Factory -) : StatsListViewModel(mainDispatcher, statsUseCase, analyticsTracker, dateSelectorFactory.build(WEEKS)) +) : StatsListViewModel( + mainDispatcher, + statsUseCase, + analyticsTracker, + dateSelectorFactory.build(StatsGranularity.WEEKS) +) class TotalCommentsDetailListViewModel @Inject constructor( @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, @Named(TOTAL_COMMENTS_DETAIL_USE_CASE) statsUseCase: BaseListUseCase, analyticsTracker: AnalyticsTrackerWrapper, dateSelectorFactory: StatsDateSelector.Factory -) : StatsListViewModel(mainDispatcher, statsUseCase, analyticsTracker, dateSelectorFactory.build(WEEKS)) +) : StatsListViewModel( + mainDispatcher, + statsUseCase, + analyticsTracker, + dateSelectorFactory.build(StatsGranularity.WEEKS) +) class TotalFollowersDetailListViewModel @Inject constructor( @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, @Named(TOTAL_FOLLOWERS_DETAIL_USE_CASE) statsUseCase: BaseListUseCase, analyticsTracker: AnalyticsTrackerWrapper, dateSelectorFactory: StatsDateSelector.Factory -) : StatsListViewModel(mainDispatcher, statsUseCase, analyticsTracker, dateSelectorFactory.build(WEEKS)) +) : StatsListViewModel( + mainDispatcher, + statsUseCase, + analyticsTracker, + dateSelectorFactory.build(StatsGranularity.WEEKS) +) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/DetailListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/DetailListViewModel.kt index 553ff06d2997..9ceb3563f701 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/DetailListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/DetailListViewModel.kt @@ -1,11 +1,11 @@ package org.wordpress.android.ui.stats.refresh.lists.detail import kotlinx.coroutines.CoroutineDispatcher +import org.wordpress.android.fluxc.network.utils.StatsGranularity import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.stats.refresh.BLOCK_DETAIL_USE_CASE import org.wordpress.android.ui.stats.refresh.lists.BaseListUseCase import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.DETAIL import org.wordpress.android.ui.stats.refresh.utils.StatsDateSelector import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import javax.inject.Inject @@ -17,9 +17,14 @@ class DetailListViewModel @Named(BLOCK_DETAIL_USE_CASE) private val detailUseCase: BaseListUseCase, analyticsTracker: AnalyticsTrackerWrapper, dateSelectorFactory: StatsDateSelector.Factory -) : StatsListViewModel(mainDispatcher, detailUseCase, analyticsTracker, dateSelectorFactory.build(DETAIL)) { +) : StatsListViewModel( + mainDispatcher, + detailUseCase, + analyticsTracker, + dateSelectorFactory.build(StatsGranularity.DAYS) +) { override fun onCleared() { super.onCleared() - dateSelector.clear() + dateSelector?.clear() } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/PostDayViewsUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/PostDayViewsUseCase.kt index c5e7c1809c52..d6dd4e02c4d6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/PostDayViewsUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/PostDayViewsUseCase.kt @@ -8,7 +8,6 @@ import org.wordpress.android.fluxc.store.StatsStore.PostDetailType import org.wordpress.android.fluxc.store.stats.PostDetailStore import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.modules.UI_THREAD -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.DETAIL import org.wordpress.android.ui.stats.refresh.lists.detail.PostDayViewsUseCase.UiState import org.wordpress.android.ui.stats.refresh.lists.sections.BaseStatsUseCase import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem @@ -39,7 +38,7 @@ class PostDayViewsUseCase mainDispatcher, backgroundDispatcher, UiState(), - uiUpdateParams = listOf(UseCaseParam.SelectedDateParam(DETAIL)) + uiUpdateParams = listOf(UseCaseParam.SelectedDateParam(DAYS)) ) { override suspend fun loadCachedData(): PostDetailStatsModel? { return statsPostProvider.postId?.let { postId -> @@ -59,15 +58,15 @@ class PostDayViewsUseCase return when { error != null -> { - selectedDateProvider.onDateLoadingFailed(DETAIL) + selectedDateProvider.onDateLoadingFailed(DAYS) State.Error(error.message ?: error.type.name) } model != null && model.hasData() -> { - selectedDateProvider.onDateLoadingSucceeded(DETAIL) + selectedDateProvider.onDateLoadingSucceeded(DAYS) State.Data(model) } else -> { - selectedDateProvider.onDateLoadingSucceeded(DETAIL) + selectedDateProvider.onDateLoadingSucceeded(DAYS) State.Empty() } } @@ -78,14 +77,14 @@ class PostDayViewsUseCase val visibleBarCount = uiState.visibleBarCount ?: domainModel.dayViews.size if (domainModel.hasData() && visibleBarCount > 0) { - val periodFromProvider = selectedDateProvider.getSelectedDate(DETAIL) + val periodFromProvider = selectedDateProvider.getSelectedDate(DAYS) val availablePeriods = domainModel.dayViews.takeLast(visibleBarCount) val availableDates = availablePeriods.map { statsDateFormatter.parseStatsDate(DAYS, it.period) } val selectedPeriod = periodFromProvider ?: availableDates.last() val index = availableDates.indexOf(selectedPeriod) - selectedDateProvider.selectDate(selectedPeriod, availableDates, DETAIL) + selectedDateProvider.selectDate(selectedPeriod, availableDates, DAYS) val shiftedIndex = index + domainModel.dayViews.size - visibleBarCount val selectedItem = domainModel.dayViews.getOrNull(shiftedIndex) ?: domainModel.dayViews.last() @@ -107,7 +106,7 @@ class PostDayViewsUseCase ) ) } else { - selectedDateProvider.onDateLoadingFailed(DETAIL) + selectedDateProvider.onDateLoadingFailed(DAYS) AppLog.e(T.STATS, "There is no data to be shown in the post day view block") } return items @@ -129,7 +128,7 @@ class PostDayViewsUseCase val selectedDate = statsDateFormatter.parseStatsDate(DAYS, period) selectedDateProvider.selectDate( selectedDate, - DETAIL + DAYS ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/BaseStatsUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/BaseStatsUseCase.kt index ec7c148fdc3c..7b86388e0e3a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/BaseStatsUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/BaseStatsUseCase.kt @@ -9,9 +9,9 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.wordpress.android.R +import org.wordpress.android.fluxc.network.utils.StatsGranularity import org.wordpress.android.fluxc.store.StatsStore.StatsType import org.wordpress.android.ui.stats.refresh.NavigationTarget -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection import org.wordpress.android.ui.stats.refresh.lists.sections.BaseStatsUseCase.State.Data import org.wordpress.android.ui.stats.refresh.lists.sections.BaseStatsUseCase.State.Empty import org.wordpress.android.ui.stats.refresh.lists.sections.BaseStatsUseCase.State.Error @@ -282,6 +282,6 @@ abstract class BaseStatsUseCase( } sealed class UseCaseParam { - data class SelectedDateParam(val statsSection: StatsSection) : UseCaseParam() + data class SelectedDateParam(val statsGranularity: StatsGranularity) : UseCaseParam() } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/GranularStatefulUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/GranularStatefulUseCase.kt index 30d1e2ae7bc3..dce50cf9b582 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/GranularStatefulUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/GranularStatefulUseCase.kt @@ -8,7 +8,6 @@ import org.wordpress.android.fluxc.store.StatsStore.StatsType import org.wordpress.android.ui.stats.refresh.lists.sections.BaseStatsUseCase import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider -import org.wordpress.android.ui.stats.refresh.utils.toStatsSection import java.util.Date @Suppress("LongParameterList") @@ -25,7 +24,7 @@ abstract class GranularStatefulUseCase( mainDispatcher, backgroundDispatcher, defaultUiState, - listOf(UseCaseParam.SelectedDateParam(statsGranularity.toStatsSection())) + listOf(UseCaseParam.SelectedDateParam(statsGranularity)) ) { abstract suspend fun loadCachedData(selectedDate: Date, site: SiteModel): DOMAIN_MODEL? diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/GranularStatelessUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/GranularStatelessUseCase.kt index 8b6ffb02ff39..f4fc676d1b99 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/GranularStatelessUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/GranularStatelessUseCase.kt @@ -8,7 +8,6 @@ import org.wordpress.android.fluxc.store.StatsStore.StatsType import org.wordpress.android.ui.stats.refresh.lists.sections.BaseStatsUseCase.StatelessUseCase import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider -import org.wordpress.android.ui.stats.refresh.utils.toStatsSection import java.util.Date abstract class GranularStatelessUseCase( @@ -22,7 +21,7 @@ abstract class GranularStatelessUseCase( type, mainDispatcher, backgroundDispatcher, - listOf(UseCaseParam.SelectedDateParam(statsGranularity.toStatsSection())) + listOf(UseCaseParam.SelectedDateParam(statsGranularity)) ) { abstract suspend fun loadCachedData(selectedDate: Date, site: SiteModel): DOMAIN_MODEL? diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/SelectedDateProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/SelectedDateProvider.kt index 6cc52b2e5572..1ce18f0db9b7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/SelectedDateProvider.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/SelectedDateProvider.kt @@ -11,17 +11,11 @@ import kotlinx.parcelize.Parcelize import org.wordpress.android.analytics.AnalyticsTracker.Stat.STATS_NEXT_DATE_TAPPED import org.wordpress.android.analytics.AnalyticsTracker.Stat.STATS_PREVIOUS_DATE_TAPPED import org.wordpress.android.fluxc.network.utils.StatsGranularity -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.DAYS -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.MONTHS -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.WEEKS -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.YEARS import org.wordpress.android.ui.stats.refresh.utils.StatsDateFormatter -import org.wordpress.android.ui.stats.refresh.utils.toStatsSection -import org.wordpress.android.ui.stats.refresh.utils.trackWithSection +import org.wordpress.android.ui.stats.refresh.utils.trackWithGranularity import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper -import org.wordpress.android.util.extensions.readListCompat import org.wordpress.android.util.extensions.getParcelableCompat +import org.wordpress.android.util.extensions.readListCompat import org.wordpress.android.util.filter import java.util.Date import javax.inject.Inject @@ -36,121 +30,100 @@ class SelectedDateProvider private val analyticsTrackerWrapper: AnalyticsTrackerWrapper ) { private val mutableDates = mutableMapOf( - DAYS to SelectedDate(loading = true), - WEEKS to SelectedDate(loading = true), - MONTHS to SelectedDate(loading = true), - YEARS to SelectedDate(loading = true) + StatsGranularity.DAYS to SelectedDate(loading = true), + StatsGranularity.WEEKS to SelectedDate(loading = true), + StatsGranularity.MONTHS to SelectedDate(loading = true), + StatsGranularity.YEARS to SelectedDate(loading = true) ) - private val selectedDateChanged = MutableLiveData() - - fun granularSelectedDateChanged(statsSection: StatsSection): LiveData { - return selectedDateChanged.filter { it?.selectedSection == statsSection } - } + private val selectedDateChanged = MutableLiveData() - fun selectDate(date: Date, statsSection: StatsSection) { - val selectedDate = getSelectedDateState(statsSection) - updateSelectedDate(selectedDate.copy(dateValue = date), statsSection) + fun granularSelectedDateChanged(statsGranularity: StatsGranularity): LiveData { + return selectedDateChanged.filter { it?.selectedGranularity == statsGranularity } } fun selectDate(date: Date, statsGranularity: StatsGranularity) { - selectDate(date, statsGranularity.toStatsSection()) + val selectedDate = getSelectedDateState(statsGranularity) + updateSelectedDate(selectedDate.copy(dateValue = date), statsGranularity) } - fun selectDate(updatedDate: Date, availableDates: List, statsSection: StatsSection) { - val selectedDate = getSelectedDateState(statsSection) + fun selectDate(updatedDate: Date, availableDates: List, statsGranularity: StatsGranularity) { + val selectedDate = getSelectedDateState(statsGranularity) if (selectedDate.dateValue != updatedDate || selectedDate.availableDates != availableDates) { updateSelectedDate( selectedDate.copy(dateValue = updatedDate, availableDates = availableDates), - statsSection + statsGranularity ) } } - fun selectDate(updatedDate: Date, availableDates: List, statsGranularity: StatsGranularity) { - selectDate(updatedDate, availableDates, statsGranularity.toStatsSection()) - } - - fun updateSelectedDate(selectedDate: SelectedDate, statsSection: StatsSection) { - val currentDate = mutableDates[statsSection] - mutableDates[statsSection] = selectedDate + fun updateSelectedDate(selectedDate: SelectedDate, statsGranularity: StatsGranularity) { + val currentDate = mutableDates[statsGranularity] + mutableDates[statsGranularity] = selectedDate if (selectedDate != currentDate) { - selectedDateChanged.postValue(SectionChange(statsSection)) + selectedDateChanged.postValue(GranularityChange(statsGranularity)) } } fun setInitialSelectedPeriod(statsGranularity: StatsGranularity, period: String) { val updatedDate = statsDateFormatter.parseStatsDate(statsGranularity, period) val selectedDate = getSelectedDateState(statsGranularity) - updateSelectedDate(selectedDate.copy(dateValue = updatedDate), statsGranularity.toStatsSection()) + updateSelectedDate(selectedDate.copy(dateValue = updatedDate), statsGranularity) } fun getSelectedDate(statsGranularity: StatsGranularity): Date? { - return getSelectedDate(statsGranularity.toStatsSection()) - } - - fun getSelectedDate(statsSection: StatsSection): Date? { - return getSelectedDateState(statsSection).dateValue + return getSelectedDateState(statsGranularity).dateValue } fun getSelectedDateState(statsGranularity: StatsGranularity): SelectedDate { - return getSelectedDateState(statsGranularity.toStatsSection()) + return mutableDates[statsGranularity] ?: SelectedDate(loading = true) } - fun getSelectedDateState(statsSection: StatsSection): SelectedDate { - return mutableDates[statsSection] ?: SelectedDate(loading = true) - } - - fun hasPreviousDate(statsSection: StatsSection): Boolean { - val selectedDate = getSelectedDateState(statsSection) + fun hasPreviousDate(statsGranularity: StatsGranularity): Boolean { + val selectedDate = getSelectedDateState(statsGranularity) return selectedDate.hasData() && selectedDate.getDateIndex() > 0 } - fun hasNextDate(statsSection: StatsSection): Boolean { - val selectedDate = getSelectedDateState(statsSection) + fun hasNextDate(statsGranularity: StatsGranularity): Boolean { + val selectedDate = getSelectedDateState(statsGranularity) return selectedDate.hasData() && selectedDate.getDateIndex() < selectedDate.availableDates.size - 1 } - fun selectPreviousDate(statsSection: StatsSection) { - val selectedDateState = getSelectedDateState(statsSection) + fun selectPreviousDate(statsGranularity: StatsGranularity) { + val selectedDateState = getSelectedDateState(statsGranularity) if (selectedDateState.hasData()) { - analyticsTrackerWrapper.trackWithSection(STATS_PREVIOUS_DATE_TAPPED, statsSection) - updateSelectedDate(selectedDateState.copy(dateValue = selectedDateState.getPreviousDate()), statsSection) + analyticsTrackerWrapper.trackWithGranularity(STATS_PREVIOUS_DATE_TAPPED, statsGranularity) + updateSelectedDate( + selectedDateState.copy(dateValue = selectedDateState.getPreviousDate()), + statsGranularity + ) } } - fun selectNextDate(statsSection: StatsSection) { - val selectedDateState = getSelectedDateState(statsSection) + fun selectNextDate(statsGranularity: StatsGranularity) { + val selectedDateState = getSelectedDateState(statsGranularity) if (selectedDateState.hasData()) { - analyticsTrackerWrapper.trackWithSection(STATS_NEXT_DATE_TAPPED, statsSection) - updateSelectedDate(selectedDateState.copy(dateValue = selectedDateState.getNextDate()), statsSection) + analyticsTrackerWrapper.trackWithGranularity(STATS_NEXT_DATE_TAPPED, statsGranularity) + updateSelectedDate(selectedDateState.copy(dateValue = selectedDateState.getNextDate()), statsGranularity) } } fun onDateLoadingFailed(statsGranularity: StatsGranularity) { - onDateLoadingFailed(statsGranularity.toStatsSection()) - } - - fun onDateLoadingFailed(statsSection: StatsSection) { - val selectedDate = getSelectedDateState(statsSection) + val selectedDate = getSelectedDateState(statsGranularity) if (selectedDate.dateValue != null && !selectedDate.error) { - updateSelectedDate(selectedDate.copy(error = true, loading = false), statsSection) + updateSelectedDate(selectedDate.copy(error = true, loading = false), statsGranularity) } else if (selectedDate.dateValue == null) { - updateSelectedDate(SelectedDate(error = true, loading = false), statsSection) + updateSelectedDate(SelectedDate(error = true, loading = false), statsGranularity) } } fun onDateLoadingSucceeded(statsGranularity: StatsGranularity) { - onDateLoadingSucceeded(statsGranularity.toStatsSection()) - } - - fun onDateLoadingSucceeded(statsSection: StatsSection) { - val selectedDate = getSelectedDateState(statsSection) + val selectedDate = getSelectedDateState(statsGranularity) if (selectedDate.dateValue != null && selectedDate.error) { - updateSelectedDate(selectedDate.copy(error = false, loading = false), statsSection) + updateSelectedDate(selectedDate.copy(error = false, loading = false), statsGranularity) } else if (selectedDate.dateValue == null) { - updateSelectedDate(SelectedDate(error = false, loading = false), statsSection) + updateSelectedDate(SelectedDate(error = false, loading = false), statsGranularity) } } @@ -161,8 +134,8 @@ class SelectedDateProvider selectedDateChanged.value = null } - fun clear(statsSection: StatsSection) { - mutableDates[statsSection] = SelectedDate(loading = true) + fun clear(statsGranularity: StatsGranularity) { + mutableDates[statsGranularity] = SelectedDate(loading = true) selectedDateChanged.value = null } @@ -173,7 +146,12 @@ class SelectedDateProvider } fun onRestoreInstanceState(savedState: Bundle) { - for (period in listOf(DAYS, WEEKS, MONTHS, YEARS)) { + for (period in listOf( + StatsGranularity.DAYS, + StatsGranularity.WEEKS, + StatsGranularity.MONTHS, + StatsGranularity.YEARS + )) { val selectedDate = savedState.getParcelableCompat(buildStateKey(period)) if (selectedDate != null) { mutableDates[period] = selectedDate @@ -181,7 +159,7 @@ class SelectedDateProvider } } - private fun buildStateKey(key: StatsSection) = SELECTED_DATE_STATE_KEY + key + private fun buildStateKey(key: StatsGranularity) = SELECTED_DATE_STATE_KEY + key @Parcelize @SuppressLint("ParcelCreator") @@ -241,5 +219,5 @@ class SelectedDateProvider } } - data class SectionChange(val selectedSection: StatsSection) + data class GranularityChange(val selectedGranularity: StatsGranularity) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt index ef7b192d6ba7..104ebebb120c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt @@ -24,7 +24,6 @@ import org.wordpress.android.ui.stats.refresh.lists.widget.WidgetUpdater.StatsWi import org.wordpress.android.ui.stats.refresh.utils.StatsDateFormatter import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider import org.wordpress.android.ui.stats.refresh.utils.StatsUtils -import org.wordpress.android.ui.stats.refresh.utils.toStatsSection import org.wordpress.android.ui.stats.refresh.utils.trackGranular import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T @@ -60,7 +59,7 @@ class OverviewUseCase constructor( mainDispatcher, backgroundDispatcher, UiState(), - uiUpdateParams = listOf(UseCaseParam.SelectedDateParam(statsGranularity.toStatsSection())) + uiUpdateParams = listOf(UseCaseParam.SelectedDateParam(statsGranularity)) ) { override fun buildLoadingItem(): List = listOf( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/AnnualSiteStatsUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/AnnualSiteStatsUseCase.kt index 3b4faa13812e..f9e5d4e8a284 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/AnnualSiteStatsUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/AnnualSiteStatsUseCase.kt @@ -4,12 +4,12 @@ import android.view.View import kotlinx.coroutines.CoroutineDispatcher import org.wordpress.android.R import org.wordpress.android.fluxc.model.stats.YearsInsightsModel +import org.wordpress.android.fluxc.network.utils.StatsGranularity import org.wordpress.android.fluxc.store.StatsStore.InsightType.ANNUAL_SITE_STATS import org.wordpress.android.fluxc.store.stats.insights.MostPopularInsightsStore import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.stats.refresh.NavigationTarget -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.ANNUAL_STATS import org.wordpress.android.ui.stats.refresh.lists.sections.BaseStatsUseCase.StatelessUseCase import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.Link @@ -59,13 +59,13 @@ class AnnualSiteStatsUseCase( override fun buildLoadingItem(): List = listOf(Title(R.string.stats_insights_this_year_site_stats)) override fun buildUiModel(domainModel: YearsInsightsModel): List { - val periodFromProvider = selectedDateProvider.getSelectedDate(ANNUAL_STATS) + val periodFromProvider = selectedDateProvider.getSelectedDate(StatsGranularity.YEARS) val availablePeriods = domainModel.years val availableDates = availablePeriods.map { yearToDate(it.year) } val selectedPeriod = periodFromProvider ?: availableDates.last() val index = availableDates.indexOf(selectedPeriod) - selectedDateProvider.selectDate(selectedPeriod, availableDates, ANNUAL_STATS) + selectedDateProvider.selectDate(selectedPeriod, availableDates, StatsGranularity.YEARS) val items = mutableListOf() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/ViewsAndVisitorsUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/ViewsAndVisitorsUseCase.kt index 930d1bd04e49..01b1e4d5d106 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/ViewsAndVisitorsUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/ViewsAndVisitorsUseCase.kt @@ -29,7 +29,6 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.insights.usecases.V import org.wordpress.android.ui.stats.refresh.lists.widget.WidgetUpdater.StatsWidgetUpdaters import org.wordpress.android.ui.stats.refresh.utils.StatsDateFormatter import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider -import org.wordpress.android.ui.stats.refresh.utils.toStatsSection import org.wordpress.android.ui.stats.refresh.utils.trackGranular import org.wordpress.android.ui.stats.refresh.utils.trackViewsVisitorsChips import org.wordpress.android.ui.stats.refresh.utils.trackWithType @@ -68,7 +67,7 @@ class ViewsAndVisitorsUseCase mainDispatcher, backgroundDispatcher, UiState(), - uiUpdateParams = listOf(UseCaseParam.SelectedDateParam(statsGranularity.toStatsSection())) + uiUpdateParams = listOf(UseCaseParam.SelectedDateParam(statsGranularity)) ) { override fun buildLoadingItem(): List = listOf( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/SelectedSectionManager.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/SelectedSectionManager.kt index a71f504a41af..fc69e6cd1837 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/SelectedSectionManager.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/SelectedSectionManager.kt @@ -10,12 +10,7 @@ import org.wordpress.android.fluxc.network.utils.StatsGranularity.MONTHS import org.wordpress.android.fluxc.network.utils.StatsGranularity.WEEKS import org.wordpress.android.fluxc.network.utils.StatsGranularity.YEARS import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.ANNUAL_STATS -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.DETAIL import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.INSIGHTS -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.TOTAL_COMMENTS_DETAIL -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.TOTAL_FOLLOWERS_DETAIL -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.TOTAL_LIKES_DETAIL import javax.inject.Inject const val SELECTED_SECTION_KEY = "SELECTED_STATS_SECTION_KEY" @@ -47,9 +42,14 @@ class SelectedSectionManager fun StatsSection.toStatsGranularity(): StatsGranularity? { return when (this) { - ANNUAL_STATS, DETAIL, TOTAL_LIKES_DETAIL, TOTAL_COMMENTS_DETAIL, TOTAL_FOLLOWERS_DETAIL, INSIGHTS -> null + StatsSection.TRAFFIC, + StatsSection.ANNUAL_STATS, + StatsSection.DETAIL, + StatsSection.TOTAL_LIKES_DETAIL, + StatsSection.TOTAL_COMMENTS_DETAIL, + StatsSection.TOTAL_FOLLOWERS_DETAIL, + StatsSection.INSIGHTS -> null StatsSection.INSIGHT_DETAIL, - StatsSection.TRAFFIC -> DAYS // Replace with TRAFFIC when it's implemented StatsSection.DAYS -> DAYS StatsSection.WEEKS -> WEEKS StatsSection.MONTHS -> MONTHS diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/SelectedTrafficGranularityManager.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/SelectedTrafficGranularityManager.kt new file mode 100644 index 000000000000..c17ec67b92aa --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/SelectedTrafficGranularityManager.kt @@ -0,0 +1,20 @@ +package org.wordpress.android.ui.stats.refresh.utils + +import android.content.SharedPreferences +import org.wordpress.android.fluxc.network.utils.StatsGranularity +import org.wordpress.android.fluxc.network.utils.StatsGranularity.DAYS +import javax.inject.Inject + +const val SELECTED_TRAFFIC_GRANULARITY_KEY = "SELECTED_TRAFFIC_GRANULARITY_KEY" + +class SelectedTrafficGranularityManager +@Inject constructor(private val sharedPrefs: SharedPreferences) { + fun getSelectedTrafficGranularity(): StatsGranularity { + val value = sharedPrefs.getString(SELECTED_TRAFFIC_GRANULARITY_KEY, DAYS.name) + return value?.let { StatsGranularity.valueOf(value) } ?: DAYS + } + + fun setSelectedTrafficGranularity(selectedTrafficGranularity: StatsGranularity) { + sharedPrefs.edit().putString(SELECTED_TRAFFIC_GRANULARITY_KEY, selectedTrafficGranularity.name).apply() + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsAnalyticsUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsAnalyticsUtils.kt index 95bb9b5e0f79..8113a9b097e7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsAnalyticsUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsAnalyticsUtils.kt @@ -4,9 +4,6 @@ import org.wordpress.android.analytics.AnalyticsTracker.Stat import org.wordpress.android.analytics.AnalyticsTracker.Stat.STATS_INSIGHTS_VIEWS_VISITORS_TOGGLED import org.wordpress.android.fluxc.network.utils.StatsGranularity import org.wordpress.android.fluxc.store.StatsStore.InsightType -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.INSIGHT_DETAIL -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.TRAFFIC import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsWidgetConfigureFragment.WidgetType import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsWidgetConfigureFragment.WidgetType.ALL_TIME_VIEWS import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsWidgetConfigureFragment.WidgetType.TODAY_VIEWS @@ -19,9 +16,6 @@ private const val DAYS_PROPERTY = "days" private const val WEEKS_PROPERTY = "weeks" private const val MONTHS_PROPERTY = "months" private const val YEARS_PROPERTY = "years" -private const val INSIGHTS_PROPERTY = "insights" -private const val DETAIL_PROPERTY = "detail" -private const val ANNUAL_STATS_PROPERTY = "annual_stats" private const val TYPE = "type" private const val TYPES = "types" private const val WIDGET_TYPE = "widget_type" @@ -30,9 +24,6 @@ private const val WEEKLY_VIEWS_WIDGET_PROPERTY = "weekly_views" private const val WEEK_TOTALS_WIDGET_PROPERTY = "week_totals" private const val ALL_TIME_WIDGET_PROPERTY = "all_time" private const val MINIFIED_WIDGET_PROPERTY = "minified" -private const val TOTAL_LIKES_PROPERTY = "total_likes_detail" -private const val TOTAL_COMMENTS_PROPERTY = "total_comments_detail" -private const val TOTAL_FOLLOWERS_PROPERTY = "total_followers_detail" private const val CHIP_VIEWS_PROPERTY = "views" private const val CHIP_VISITORS__PROPERTY = "visitors" @@ -54,20 +45,8 @@ fun AnalyticsTrackerWrapper.trackViewsVisitorsChips(position: Int) { this.track(STATS_INSIGHTS_VIEWS_VISITORS_TOGGLED, mapOf(TYPE to property)) } -fun AnalyticsTrackerWrapper.trackWithSection(stat: Stat, section: StatsSection) { - val property = when (section) { - StatsSection.DAYS, TRAFFIC -> DAYS_PROPERTY // Replace with TRAFFIC when it's implemented - StatsSection.WEEKS -> WEEKS_PROPERTY - StatsSection.MONTHS -> MONTHS_PROPERTY - StatsSection.YEARS -> YEARS_PROPERTY - StatsSection.INSIGHTS, INSIGHT_DETAIL -> INSIGHTS_PROPERTY - StatsSection.DETAIL -> DETAIL_PROPERTY - StatsSection.ANNUAL_STATS -> ANNUAL_STATS_PROPERTY - StatsSection.TOTAL_LIKES_DETAIL -> TOTAL_LIKES_PROPERTY - StatsSection.TOTAL_COMMENTS_DETAIL -> TOTAL_COMMENTS_PROPERTY - StatsSection.TOTAL_FOLLOWERS_DETAIL -> TOTAL_FOLLOWERS_PROPERTY - } - this.track(stat, mapOf(GRANULARITY_PROPERTY to property)) +fun AnalyticsTrackerWrapper.trackWithGranularity(stat: Stat, granularity: StatsGranularity) { + this.track(stat, mapOf(GRANULARITY_PROPERTY to granularity)) } fun AnalyticsTrackerWrapper.trackWithType(stat: Stat, insightType: InsightType) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateSelector.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateSelector.kt index 89a34a9786bf..bf7d89e37a20 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateSelector.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateSelector.kt @@ -3,14 +3,7 @@ package org.wordpress.android.ui.stats.refresh.utils import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import org.wordpress.android.fluxc.network.utils.StatsGranularity -import org.wordpress.android.fluxc.network.utils.StatsGranularity.DAYS -import org.wordpress.android.fluxc.network.utils.StatsGranularity.MONTHS -import org.wordpress.android.fluxc.network.utils.StatsGranularity.WEEKS -import org.wordpress.android.fluxc.network.utils.StatsGranularity.YEARS import org.wordpress.android.ui.stats.refresh.StatsViewModel.DateSelectorUiModel -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.INSIGHTS -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.TRAFFIC import org.wordpress.android.ui.stats.refresh.lists.sections.granular.SelectedDateProvider import org.wordpress.android.ui.stats.refresh.lists.sections.granular.SelectedDateProvider.SelectedDate import org.wordpress.android.util.config.StatsTrafficTabFeatureConfig @@ -22,45 +15,39 @@ constructor( private val selectedDateProvider: SelectedDateProvider, private val statsDateFormatter: StatsDateFormatter, private val siteProvider: StatsSiteProvider, - private val statsTrafficTabFeatureConfig: StatsTrafficTabFeatureConfig, - private val statsSection: StatsSection + private val statsGranularity: StatsGranularity, + private val isGranularitySpinnerVisible: Boolean, + private val statsTrafficTabFeatureConfig: StatsTrafficTabFeatureConfig ) { private val _dateSelectorUiModel = MutableLiveData() val dateSelectorData: LiveData = _dateSelectorUiModel - val selectedDate = selectedDateProvider.granularSelectedDateChanged(this.statsSection) + val selectedDate = selectedDateProvider.granularSelectedDateChanged(statsGranularity) .perform { updateDateSelector() } fun start(startDate: SelectedDate) { - selectedDateProvider.updateSelectedDate(startDate, statsSection) + selectedDateProvider.updateSelectedDate(startDate, statsGranularity) } fun updateDateSelector() { - val shouldShowDateSelection = this.statsSection != INSIGHTS - val shouldShowGranularitySpinner = statsTrafficTabFeatureConfig.isEnabled() && this.statsSection == TRAFFIC - val updatedDate = getDateLabelForSection() val currentState = dateSelectorData.value - if (!shouldShowDateSelection && currentState?.isVisible != false) { - emitValue(currentState, DateSelectorUiModel(false)) + val timeZone = if (statsTrafficTabFeatureConfig.isEnabled()) { + null } else { - val timeZone = if (statsTrafficTabFeatureConfig.isEnabled()) { - null - } else { - statsDateFormatter.printTimeZone(siteProvider.siteModel) - } - val updatedState = DateSelectorUiModel( - shouldShowDateSelection, - shouldShowGranularitySpinner, - updatedDate, - enableSelectPrevious = selectedDateProvider.hasPreviousDate(statsSection), - enableSelectNext = selectedDateProvider.hasNextDate(statsSection), - timeZone = timeZone - ) - emitValue(currentState, updatedState) + statsDateFormatter.printTimeZone(siteProvider.siteModel) } + val updatedState = DateSelectorUiModel( + true, + isGranularitySpinnerVisible, + updatedDate, + enableSelectPrevious = selectedDateProvider.hasPreviousDate(statsGranularity), + enableSelectNext = selectedDateProvider.hasNextDate(statsGranularity), + timeZone = timeZone + ) + emitValue(currentState, updatedState) } private fun emitValue( @@ -74,41 +61,25 @@ constructor( private fun getDateLabelForSection(): String? { return statsDateFormatter.printGranularDate( - selectedDateProvider.getSelectedDate(statsSection) ?: selectedDateProvider.getCurrentDate(), - toStatsGranularity() + selectedDateProvider.getSelectedDate(statsGranularity) ?: selectedDateProvider.getCurrentDate(), + statsGranularity ) } - private fun toStatsGranularity(): StatsGranularity { - return when (statsSection) { - StatsSection.DETAIL, - StatsSection.TOTAL_LIKES_DETAIL, - StatsSection.TOTAL_COMMENTS_DETAIL, - StatsSection.TOTAL_FOLLOWERS_DETAIL, - StatsSection.INSIGHTS, - StatsSection.INSIGHT_DETAIL, - StatsSection.DAYS, TRAFFIC -> DAYS // Replace with TRAFFIC when it's implemented - StatsSection.WEEKS -> WEEKS - StatsSection.MONTHS -> MONTHS - StatsSection.ANNUAL_STATS, - StatsSection.YEARS -> YEARS - } - } - fun onNextDateSelected() { - selectedDateProvider.selectNextDate(statsSection) + selectedDateProvider.selectNextDate(statsGranularity) } fun onPreviousDateSelected() { - selectedDateProvider.selectPreviousDate(statsSection) + selectedDateProvider.selectPreviousDate(statsGranularity) } fun clear() { - selectedDateProvider.clear(statsSection) + selectedDateProvider.clear(statsGranularity) } fun getSelectedDate(): SelectedDate { - return selectedDateProvider.getSelectedDateState(statsSection) + return selectedDateProvider.getSelectedDateState(statsGranularity) } class Factory @@ -118,13 +89,14 @@ constructor( private val statsDateFormatter: StatsDateFormatter, private val statsTrafficTabFeatureConfig: StatsTrafficTabFeatureConfig ) { - fun build(statsSection: StatsSection): StatsDateSelector { + fun build(statsGranularity: StatsGranularity, isGranularitySpinnerVisible: Boolean = false): StatsDateSelector { return StatsDateSelector( selectedDateProvider, statsDateFormatter, siteProvider, - statsTrafficTabFeatureConfig, - statsSection + statsGranularity, + isGranularitySpinnerVisible, + statsTrafficTabFeatureConfig ) } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorMapperTest.kt new file mode 100644 index 000000000000..d9b349bc2873 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorMapperTest.kt @@ -0,0 +1,58 @@ +package org.wordpress.android.ui.sitemonitor + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest + +@ExperimentalCoroutinesApi +@RunWith(MockitoJUnitRunner::class) +class SiteMonitorMapperTest : BaseUnitTest() { + @Mock + lateinit var siteMonitorUtils: SiteMonitorUtils + + private lateinit var siteMonitorMapper: SiteMonitorMapper + + @Before + fun setup() { + siteMonitorMapper = SiteMonitorMapper(siteMonitorUtils) + } + + @Test + fun `given prepared request, when mapper is called, then site monitor model is created`() { + whenever(siteMonitorUtils.getUserAgent()).thenReturn(USER_AGENT) + + val state = siteMonitorMapper.toPrepared(URL, ADDRESS_TO_LOAD, SiteMonitorType.METRICS) + + assertThat(state.model.siteMonitorType).isEqualTo(SiteMonitorType.METRICS) + assertThat(state.model.url).isEqualTo(URL) + assertThat(state.model.addressToLoad).isEqualTo(ADDRESS_TO_LOAD) + assertThat(state.model.userAgent).isEqualTo(USER_AGENT) + } + + @Test + fun `given network error, when mapper is called, then NoNetwork error is created`() { + val state = siteMonitorMapper.toNoNetworkError(mock()) + + assertThat(state).isInstanceOf(SiteMonitorUiState.NoNetworkError::class.java) + } + + @Test + fun `given generic error error, when mapper is called, then Generic error is created`() { + val state = siteMonitorMapper.toGenericError(mock()) + + assertThat(state).isInstanceOf(SiteMonitorUiState.GenericError::class.java) + } + + companion object { + const val USER_AGENT = "user_agent" + const val URL = "url" + const val ADDRESS_TO_LOAD = "address_to_load" + } +} 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 new file mode 100644 index 000000000000..411f72a9a442 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModelTest.kt @@ -0,0 +1,170 @@ +package org.wordpress.android.ui.sitemonitor + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +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.any +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.fluxc.model.SiteModel + +@ExperimentalCoroutinesApi +@RunWith(MockitoJUnitRunner::class) +class SiteMonitorParentViewModelTest: BaseUnitTest(){ + @Mock + private lateinit var siteMonitorUtils: SiteMonitorUtils + @Mock + private lateinit var metricsViewModel: SiteMonitorTabViewModelSlice + @Mock + private lateinit var phpLogViewModel: SiteMonitorTabViewModelSlice + @Mock + private lateinit var webServerViewModel: SiteMonitorTabViewModelSlice + + private lateinit var viewModel: SiteMonitorParentViewModel + + @Before + fun setUp() { + viewModel = SiteMonitorParentViewModel( + testDispatcher(), + siteMonitorUtils, + metricsViewModel, + phpLogViewModel, + webServerViewModel + ) + } + + @Test + fun `when viewmodel is started, then track screen shown`() { + val site = mock() + viewModel.start(site) + + verify(siteMonitorUtils).trackActivityLaunched() + } + + @Test + fun `when viewmodel is created, then view model slices are initialized`() { + verify(metricsViewModel).initialize(any()) + verify(phpLogViewModel).initialize(any()) + verify(webServerViewModel).initialize(any()) + } + + @Test + fun `when start is invoked, then view models are started with the correct tab item`() { + val site = mock() + viewModel.start(site) + + verify(metricsViewModel).start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site) + verify(phpLogViewModel).start(SiteMonitorType.PHP_LOGS, SiteMonitorTabItem.PHPLogs.urlTemplate, site) + verify(webServerViewModel).start( + SiteMonitorType.WEB_SERVER_LOGS, + SiteMonitorTabItem.WebServerLogs.urlTemplate, + site + ) + } + + @Test + fun `when loadData is invoked, then view models are started with the correct tab item`() { + val site = mock() + viewModel.start(site) + + clearInvocations(metricsViewModel, phpLogViewModel, webServerViewModel) + + viewModel.loadData() + + verify(metricsViewModel).start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site) + verify(phpLogViewModel).start(SiteMonitorType.PHP_LOGS, SiteMonitorTabItem.PHPLogs.urlTemplate, site) + verify(webServerViewModel).start( + SiteMonitorType.WEB_SERVER_LOGS, + SiteMonitorTabItem.WebServerLogs.urlTemplate, + site + ) + } + + @Test + fun `given metrics, when getUiState is invoked, then ui state is returned`() { + whenever(metricsViewModel.uiState).thenReturn(mock()) + val site = mock() + viewModel.start(site) + + advanceUntilIdle() + + val state = viewModel.getUiState(SiteMonitorType.METRICS) + + assertThat(state).isNotNull + } + + @Test + fun `given phplogs, when getUiState is invoked, then ui state is returned`() { + whenever(phpLogViewModel.uiState).thenReturn(mock()) + val site = mock() + viewModel.start(site) + + advanceUntilIdle() + + val state = viewModel.getUiState(SiteMonitorType.PHP_LOGS) + + assertThat(state).isNotNull + } + + @Test + fun `given webserver logs, when getUiState is invoked, then ui state is returned`() { + whenever(webServerViewModel.uiState).thenReturn(mock()) + val site = mock() + viewModel.start(site) + + advanceUntilIdle() + + val state = viewModel.getUiState(SiteMonitorType.WEB_SERVER_LOGS) + + assertThat(state).isNotNull + } + + @Test + fun `given metrics, when onUrlLoaded is invoked, then metric vm slice onUrlLoaded is invoked`() { + viewModel.onUrlLoaded(SiteMonitorType.METRICS) + + verify(metricsViewModel).onUrlLoaded() + } + + @Test + fun `given php logs, when onUrlLoaded is invoked, then php logs vm slice onUrlLoaded is invoked`() { + viewModel.onUrlLoaded(SiteMonitorType.PHP_LOGS) + + verify(phpLogViewModel).onUrlLoaded() + } + + @Test + fun `given webserver logs, when onUrlLoaded is invoked, then webserver logs vm slice onUrlLoaded is invoked`() { + viewModel.onUrlLoaded(SiteMonitorType.WEB_SERVER_LOGS) + + verify(webServerViewModel).onUrlLoaded() + } + + @Test + fun `given metrics, when onWebViewError is invoked, then metric vm slice onWebViewError is invoked`() { + viewModel.onWebViewError(SiteMonitorType.METRICS) + + verify(metricsViewModel).onWebViewError() + } + + @Test + fun `given php logs, when onWebViewError is invoked, then php logs vm slice onWebViewError is invoked`() { + viewModel.onWebViewError(SiteMonitorType.PHP_LOGS) + + verify(phpLogViewModel).onWebViewError() + } + + @Test + fun `given webserver logs, when onWebViewError is invoked, then webserver vm slice onWebViewError is invoked`() { + viewModel.onWebViewError(SiteMonitorType.WEB_SERVER_LOGS) + + verify(webServerViewModel).onWebViewError() + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSliceTest.kt new file mode 100644 index 000000000000..3fe113c67fb7 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSliceTest.kt @@ -0,0 +1,127 @@ +package org.wordpress.android.ui.sitemonitor + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +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.util.NetworkUtilsWrapper + +@ExperimentalCoroutinesApi +class SiteMonitorTabViewModelSliceTest : BaseUnitTest() { + @Mock + private lateinit var networkUtilsWrapper: NetworkUtilsWrapper + @Mock + private lateinit var accountStore: AccountStore + @Mock + private lateinit var mapper: SiteMonitorMapper + @Mock + private lateinit var siteMonitorUtils: SiteMonitorUtils + @Mock + private lateinit var siteStore: SiteStore + + private lateinit var viewModel: SiteMonitorTabViewModelSlice + + val site = mock() + + @Before + fun setUp() = test { + viewModel = SiteMonitorTabViewModelSlice( + networkUtilsWrapper, + accountStore, + mapper, + siteMonitorUtils, + siteStore + ) + + whenever(accountStore.account).thenReturn(mock()) + whenever(accountStore.account.userName).thenReturn(USER_NAME) + whenever(accountStore.accessToken).thenReturn(ACCESS_TOKEN) + + whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true) + whenever(mapper.toGenericError(any())).thenReturn(mock()) + whenever(mapper.toNoNetworkError(any())).thenReturn(mock()) + whenever(mapper.toPrepared(any(), any(), any())).thenReturn(mock()) + + whenever(site.url).thenReturn(URL) + whenever(siteMonitorUtils.sanitizeSiteUrl(any())).thenReturn(URL) + whenever(siteMonitorUtils.getAuthenticationPostData(any(), any(), any(), any(), any())).thenReturn(URL) + + viewModel.initialize(testScope()) + } + + @Test + fun `when slice is instantiated, then uiState is in preparing`() { + assertThat(viewModel.uiState.value).isEqualTo(SiteMonitorUiState.Preparing) + } + + @Test + fun `given loadView(), when slice is started, then uiState is in prepared`() = test { + viewModel.start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site) + + assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.Prepared::class.java) + } + + @Test + fun `given null username, when slice is started, then uiState is in toGenericError`() { + whenever(accountStore.account.userName).thenReturn(null) + + viewModel.start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site) + + assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.GenericError::class.java) + } + + @Test + fun `given null accessToken, when slice is started, then uiState is in toGenericError`() { + whenever(accountStore.accessToken).thenReturn(null) + + viewModel.start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site) + + assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.GenericError::class.java) + } + + @Test + fun `given no network, when slice is started, then uiState is in error`() { + whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false) + + viewModel.start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site) + + assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.NoNetworkError::class.java) + } + + @Test + fun `given prepared state, when url is loaded, then uiState loaded is posted`() = test { + viewModel.start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site) + advanceUntilIdle() + viewModel.onUrlLoaded() + + assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.Loaded::class.java) + } + + @Test + fun `given preparing state, when url is loaded, then uiState loaded is not posted`() = test { + viewModel.onUrlLoaded() + + assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.Preparing::class.java) + } + + @Test + fun `when web view error, then error state is posted`() = test { + viewModel.onWebViewError() + + assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.GenericError::class.java) + } + + companion object { + const val USER_NAME = "user_name" + const val ACCESS_TOKEN = "access_token" + const val URL = "test.wordpress.com" + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtilsTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtilsTest.kt new file mode 100644 index 000000000000..0bf5076d32d5 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtilsTest.kt @@ -0,0 +1,92 @@ +package org.wordpress.android.ui.sitemonitor + +import junit.framework.TestCase.assertEquals +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.verify +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper + +@RunWith(MockitoJUnitRunner::class) +class SiteMonitorUtilsTest { + @Mock + lateinit var analyticsTrackerWrapper: AnalyticsTrackerWrapper + + private lateinit var siteMonitorUtils: SiteMonitorUtils + + @Before + fun setup() { + siteMonitorUtils = SiteMonitorUtils(analyticsTrackerWrapper) + } + + @Test + fun `when activity is launched, then event is tracked`() { + siteMonitorUtils.trackActivityLaunched() + + verify(analyticsTrackerWrapper).track(AnalyticsTracker.Stat.SITE_MONITORING_SCREEN_SHOWN) + } + + @Test + fun `given url matches pattern, when sanitize is requested, then url is sanitized`() { + val result = siteMonitorUtils.sanitizeSiteUrl("http://example.com") + + assertEquals("example.com", result) + } + + @Test + fun `given url is null, when sanitize is requested, then url is empty`() { + val result = siteMonitorUtils.sanitizeSiteUrl(null) + + assertEquals("", result) + } + + @Test + fun `given url does not match pattern, when sanitize is requested, then url is not sanitized`() { + val url = "gibberish" + val result = siteMonitorUtils.sanitizeSiteUrl(url) + + assertEquals(url, result) + } + + @Test + fun `when metrics tab is launched, then event is tracked`() { + siteMonitorUtils.trackTabLoaded(SiteMonitorType.METRICS) + + // Verify that the correct method was called on the analyticsTrackerWrapper + verify(analyticsTrackerWrapper).track( + AnalyticsTracker.Stat.SITE_MONITORING_TAB_SHOWN, + mapOf( + SiteMonitorUtils.TAB_TRACK_KEY to SiteMonitorType.METRICS.analyticsDescription + ) + ) + } + + @Test + fun `when php logs tab is launched, then event is tracked`() { + siteMonitorUtils.trackTabLoaded(SiteMonitorType.PHP_LOGS) + + // Verify that the correct method was called on the analyticsTrackerWrapper + verify(analyticsTrackerWrapper).track( + AnalyticsTracker.Stat.SITE_MONITORING_TAB_SHOWN, + mapOf( + SiteMonitorUtils.TAB_TRACK_KEY to SiteMonitorType.PHP_LOGS.analyticsDescription + ) + ) + } + + @Test + fun `when web server logs tab is launched, then event is tracked`() { + siteMonitorUtils.trackTabLoaded(SiteMonitorType.WEB_SERVER_LOGS) + + // Verify that the correct method was called on the analyticsTrackerWrapper + verify(analyticsTrackerWrapper).track( + AnalyticsTracker.Stat.SITE_MONITORING_TAB_SHOWN, + mapOf( + SiteMonitorUtils.TAB_TRACK_KEY to SiteMonitorType.WEB_SERVER_LOGS.analyticsDescription + ) + ) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClientTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClientTest.kt new file mode 100644 index 000000000000..38b11dbd998d --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClientTest.kt @@ -0,0 +1,70 @@ +package org.wordpress.android.ui.sitemonitor + +import android.net.Uri +import android.webkit.WebResourceRequest +import android.webkit.WebView +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import android.webkit.WebResourceError +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.mock +import org.mockito.kotlin.never + +@ExperimentalCoroutinesApi +class SiteMonitorWebViewClientTest : BaseUnitTest() { + @Mock + private lateinit var mockListener: SiteMonitorWebViewClient.SiteMonitorWebViewClientListener + + @Mock + private lateinit var uri: Uri + + private lateinit var webViewClient: SiteMonitorWebViewClient + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + webViewClient = SiteMonitorWebViewClient(mockListener, SiteMonitorType.METRICS) + } + + @Test + fun `when onPageFinished, then should invoke on web view page loaded`() { + webViewClient.onPageFinished(mock(WebView::class.java), "https://example.com") + + verify(mockListener).onWebViewPageLoaded("https://example.com", SiteMonitorType.METRICS) + } + + @Test + fun `when onReceivedError, then should invoke on web view error received`() { + val mockRequest = mock(WebResourceRequest::class.java) + whenever(mockRequest.isForMainFrame).thenReturn(true) + val url = "https://some.domain" + whenever(uri.toString()).thenReturn(url) + whenever(mockRequest.url).thenReturn(uri) + + webViewClient.onPageStarted(mock(WebView::class.java), url, null) + webViewClient.onReceivedError( + mock(WebView::class.java), + mockRequest, + mock(WebResourceError::class.java) + ) + + verify(mockListener).onWebViewReceivedError(url, SiteMonitorType.METRICS) + } + + @Test + fun `when onPageFinished, then should not invoke OnReceivedError`() { + val url = "https://some.domain" + + webViewClient.onPageFinished(mock(WebView::class.java), url) + + verify(mockListener, never()).onWebViewReceivedError(anyString(), any()) + } +} + diff --git a/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/StatsDateSelectorTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/StatsDateSelectorTest.kt index dd6096d09abd..af7fd0adf5f2 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/StatsDateSelectorTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/StatsDateSelectorTest.kt @@ -10,9 +10,8 @@ import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.fluxc.network.utils.StatsGranularity import org.wordpress.android.ui.stats.refresh.StatsViewModel.DateSelectorUiModel -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection import org.wordpress.android.ui.stats.refresh.lists.sections.granular.SelectedDateProvider -import org.wordpress.android.ui.stats.refresh.lists.sections.granular.SelectedDateProvider.SectionChange +import org.wordpress.android.ui.stats.refresh.lists.sections.granular.SelectedDateProvider.GranularityChange import org.wordpress.android.ui.stats.refresh.utils.StatsDateFormatter import org.wordpress.android.ui.stats.refresh.utils.StatsDateSelector import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider @@ -34,28 +33,29 @@ class StatsDateSelectorTest : BaseUnitTest() { lateinit var statsTrafficTabFeatureConfig: StatsTrafficTabFeatureConfig private val selectedDate = Date(0) private val selectedDateLabel = "Jan 1" - private val statsSection = StatsSection.DAYS private val statsGranularity = StatsGranularity.DAYS private val updatedDate = Date(10) private val updatedLabel = "Jan 2" - private val dateProviderSelectedDate = MutableLiveData() + private val dateProviderSelectedDate = MutableLiveData() private lateinit var dateSelector: StatsDateSelector @Before fun setUp() { - dateProviderSelectedDate.value = SectionChange(statsSection) - whenever(selectedDateProvider.granularSelectedDateChanged(statsSection)).thenReturn(dateProviderSelectedDate) + dateProviderSelectedDate.value = GranularityChange(statsGranularity) + whenever(selectedDateProvider.granularSelectedDateChanged(statsGranularity)) + .thenReturn(dateProviderSelectedDate) dateSelector = StatsDateSelector( selectedDateProvider, statsDateFormatter, siteProvider, - statsTrafficTabFeatureConfig, - statsSection + statsGranularity, + false, + statsTrafficTabFeatureConfig ) - whenever(selectedDateProvider.getSelectedDate(statsSection)).thenReturn(selectedDate) + whenever(selectedDateProvider.getSelectedDate(statsGranularity)).thenReturn(selectedDate) whenever(statsDateFormatter.printGranularDate(selectedDate, statsGranularity)).thenReturn(selectedDateLabel) whenever(statsDateFormatter.printGranularDate(updatedDate, statsGranularity)).thenReturn(updatedLabel) whenever(statsTrafficTabFeatureConfig.isEnabled()).thenReturn(true) @@ -77,9 +77,9 @@ class StatsDateSelectorTest : BaseUnitTest() { @Test fun `shows date selector on days screen`() { - whenever(selectedDateProvider.getSelectedDate(statsSection)).thenReturn(selectedDate) - whenever(selectedDateProvider.hasPreviousDate(statsSection)).thenReturn(true) - whenever(selectedDateProvider.hasNextDate(statsSection)).thenReturn(true) + whenever(selectedDateProvider.getSelectedDate(statsGranularity)).thenReturn(selectedDate) + whenever(selectedDateProvider.hasPreviousDate(statsGranularity)).thenReturn(true) + whenever(selectedDateProvider.hasNextDate(statsGranularity)).thenReturn(true) var model: DateSelectorUiModel? = null dateSelector.dateSelectorData.observeForever { model = it } @@ -95,8 +95,8 @@ class StatsDateSelectorTest : BaseUnitTest() { @Test fun `updates date selector on date change`() { - whenever(selectedDateProvider.hasPreviousDate(statsSection)).thenReturn(true) - whenever(selectedDateProvider.hasNextDate(statsSection)).thenReturn(true) + whenever(selectedDateProvider.hasPreviousDate(statsGranularity)).thenReturn(true) + whenever(selectedDateProvider.hasNextDate(statsGranularity)).thenReturn(true) var model: DateSelectorUiModel? = null dateSelector.dateSelectorData.observeForever { model = it } @@ -104,31 +104,10 @@ class StatsDateSelectorTest : BaseUnitTest() { Assertions.assertThat(model?.date).isEqualTo(selectedDateLabel) - whenever(selectedDateProvider.getSelectedDate(statsSection)).thenReturn(updatedDate) + whenever(selectedDateProvider.getSelectedDate(statsGranularity)).thenReturn(updatedDate) dateSelector.updateDateSelector() Assertions.assertThat(model?.date).isEqualTo(updatedLabel) } - - @Test - fun `verify date selector hidden for insights`() { - whenever(selectedDateProvider.granularSelectedDateChanged(StatsSection.INSIGHTS)).thenReturn( - dateProviderSelectedDate - ) - dateSelector = StatsDateSelector( - selectedDateProvider, - statsDateFormatter, - siteProvider, - statsTrafficTabFeatureConfig, - StatsSection.INSIGHTS - ) - var model: DateSelectorUiModel? = null - dateSelector.dateSelectorData.observeForever { model = it } - - dateSelector.updateDateSelector() - - Assertions.assertThat(model).isNotNull - Assertions.assertThat(model?.isVisible).isFalse() - } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/AnnualSiteStatsUseCaseTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/AnnualSiteStatsUseCaseTest.kt index 260266fd77dd..22bddae81009 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/AnnualSiteStatsUseCaseTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/usecases/AnnualSiteStatsUseCaseTest.kt @@ -13,12 +13,12 @@ import org.wordpress.android.R import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.stats.YearsInsightsModel import org.wordpress.android.fluxc.model.stats.YearsInsightsModel.YearInsights +import org.wordpress.android.fluxc.network.utils.StatsGranularity import org.wordpress.android.fluxc.store.StatsStore.OnStatsFetched import org.wordpress.android.fluxc.store.StatsStore.StatsError import org.wordpress.android.fluxc.store.StatsStore.StatsErrorType.GENERIC_ERROR import org.wordpress.android.fluxc.store.stats.insights.MostPopularInsightsStore import org.wordpress.android.ui.stats.refresh.NavigationTarget -import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection.ANNUAL_STATS import org.wordpress.android.ui.stats.refresh.lists.sections.BaseStatsUseCase.UseCaseMode.BLOCK import org.wordpress.android.ui.stats.refresh.lists.sections.BaseStatsUseCase.UseCaseMode.VIEW_ALL import org.wordpress.android.ui.stats.refresh.lists.sections.BaseStatsUseCase.UseCaseModel @@ -107,7 +107,11 @@ class AnnualSiteStatsUseCaseTest : BaseUnitTest() { selectedDate.set(Calendar.YEAR, 2019) selectedDate.set(Calendar.MONTH, Calendar.DECEMBER) selectedDate.set(Calendar.DAY_OF_MONTH, 31) - verify(selectedDateProvider, times(1)).selectDate(selectedDate.time, listOf(selectedDate.time), ANNUAL_STATS) + verify(selectedDateProvider, times(1)).selectDate( + selectedDate.time, + listOf(selectedDate.time), + StatsGranularity.YEARS + ) } @Test diff --git a/build.gradle b/build.gradle index 284bcf07204d..b909a47e97fc 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ ext { automatticRestVersion = '1.0.8' automatticStoriesVersion = '2.4.0' automatticTracksVersion = '3.3.0' - gutenbergMobileVersion = 'v1.112.0-alpha3' + gutenbergMobileVersion = 'v1.112.0-alpha5' wordPressAztecVersion = 'v2.0' wordPressFluxCVersion = '2.64.0' wordPressLoginVersion = '1.11.0' @@ -32,7 +32,8 @@ ext { indexosMediaForMobileVersion = '43a9026f0973a2f0a74fa813132f6a16f7499c3a' // debug - stethoVersion = '1.6.0' + flipperVersion = '0.245.0' + soLoaderVersion = '0.10.5' // main androidInstallReferrerVersion = '2.2' @@ -88,7 +89,7 @@ ext { zendeskVersion = '5.1.2' // react native - facebookReactVersion = '0.71.11' + facebookReactVersion = '0.71.15' // test assertjVersion = '3.23.1' diff --git a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java index 4ecc68844c66..9c93b7293a73 100644 --- a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java +++ b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java @@ -1104,6 +1104,7 @@ public enum Stat { SITE_MONITORING_SCREEN_SHOWN, OPENED_SITE_MONITORING, SITE_MONITORING_TAB_SHOWN, + SITE_MONITORING_TAB_LOADING_ERROR } private static final List TRACKERS = new ArrayList<>(); diff --git a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java index 6bc51ec5bdbf..5e5fa351b389 100644 --- a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java +++ b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java @@ -2701,6 +2701,8 @@ public static String getEventNameForStat(AnalyticsTracker.Stat stat) { return "opened_site_monitoring"; case SITE_MONITORING_TAB_SHOWN: return "site_monitoring_tab_shown"; + case SITE_MONITORING_TAB_LOADING_ERROR: + return "site_monitoring_tab_loading_error"; } return null; }