Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
import org.wordpress.android.ui.posts.AddCategoryFragment;
import org.wordpress.android.ui.posts.EditPostActivity;
import org.wordpress.android.ui.posts.GutenbergKitActivity;
import org.wordpress.android.ui.posts.editor.GutenbergKitEditorFragment;
import org.wordpress.android.ui.posts.EditPostPublishSettingsFragment;
import org.wordpress.android.ui.posts.EditPostSettingsFragment;
import org.wordpress.android.ui.posts.HistoryListFragment;
Expand Down Expand Up @@ -255,6 +256,8 @@ public interface AppComponent {

void inject(GutenbergKitActivity object);

void inject(GutenbergKitEditorFragment object);

void inject(EditPostSettingsFragment object);

void inject(PostSettingsListDialogFragment object);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import org.wordpress.android.ui.mysite.items.DashboardItemsViewModelSlice
import org.wordpress.android.ui.pages.SnackbarMessageHolder
import org.wordpress.android.ui.mediapicker.MediaPickerActivity
import org.wordpress.android.ui.posts.BasicDialogViewModel
import org.wordpress.android.repositories.EditorSettingsRepository
import org.wordpress.android.ui.posts.GutenbergEditorPreloader
import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource
import org.wordpress.android.util.BuildConfigWrapper
import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
Expand All @@ -43,9 +45,7 @@ import javax.inject.Inject
import javax.inject.Named
import org.wordpress.android.ui.mysite.cards.applicationpassword.ApplicationPasswordViewModelSlice
import org.wordpress.android.ui.mysite.items.listitem.SiteCapabilityChecker
import org.wordpress.android.ui.posts.GutenbergKitWarmupHelper
import org.wordpress.android.ui.utils.UiString
import org.wordpress.android.repositories.EditorSettingsRepository

@Suppress("LargeClass", "LongMethod", "LongParameterList")
class MySiteViewModel @Inject constructor(
Expand All @@ -65,8 +65,8 @@ class MySiteViewModel @Inject constructor(
private val dashboardCardsViewModelSlice: DashboardCardsViewModelSlice,
private val dashboardItemsViewModelSlice: DashboardItemsViewModelSlice,
private val applicationPasswordViewModelSlice: ApplicationPasswordViewModelSlice,
private val gutenbergKitWarmupHelper: GutenbergKitWarmupHelper,
private val siteCapabilityChecker: SiteCapabilityChecker,
private val gutenbergEditorPreloader: GutenbergEditorPreloader,
private val editorSettingsRepository: EditorSettingsRepository,
) : ScopedViewModel(mainDispatcher) {
private val _onSnackbarMessage = MutableLiveData<Event<SnackbarMessageHolder>>()
Expand Down Expand Up @@ -169,7 +169,7 @@ class MySiteViewModel @Inject constructor(
if (isPullToRefresh) {
siteCapabilityChecker.clearCacheForSite(site.siteId)
}
buildDashboardOrSiteItems(site)
buildDashboardOrSiteItems(site, forceRefresh = isPullToRefresh)
launch {
fetchEditorCapabilitiesWithSnackbar(
site,
Expand Down Expand Up @@ -211,8 +211,7 @@ class MySiteViewModel @Inject constructor(
Event(
SnackbarMessageHolder(
UiString.UiStringRes(
R.string
.site_settings_fetch_failed
R.string.site_settings_fetch_failed
)
)
)
Expand Down Expand Up @@ -284,7 +283,7 @@ class MySiteViewModel @Inject constructor(
dashboardCardsViewModelSlice.onCleared()
dashboardItemsViewModelSlice.onCleared()
accountDataViewModelSlice.onCleared()
gutenbergKitWarmupHelper.clearWarmupState()
gutenbergEditorPreloader.clear()
super.onCleared()
}

Expand Down Expand Up @@ -317,7 +316,10 @@ class MySiteViewModel @Inject constructor(
}
}

private fun buildDashboardOrSiteItems(site: SiteModel) {
private fun buildDashboardOrSiteItems(
site: SiteModel,
forceRefresh: Boolean = false
) {
siteInfoHeaderCardViewModelSlice.buildCard(site)
applicationPasswordViewModelSlice.buildCard(site)
if (shouldShowDashboard(site)) {
Expand All @@ -327,8 +329,11 @@ class MySiteViewModel @Inject constructor(
dashboardItemsViewModelSlice.buildItems(site)
dashboardCardsViewModelSlice.clearValue()
}
// Trigger GutenbergView warmup for the selected site
gutenbergKitWarmupHelper.warmupIfNeeded(site, viewModelScope)
if (forceRefresh) {
gutenbergEditorPreloader.refreshPreloading(site, viewModelScope)
} else {
gutenbergEditorPreloader.preloadIfNeeded(site, viewModelScope)
}
}

private fun onSitePicked(site: SiteModel) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.wordpress.android.ui.posts

import android.content.Context
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import org.wordpress.gutenberg.model.EditorConfiguration
import org.wordpress.gutenberg.model.EditorDependencies

/**
* Abstracts the creation and preparation of the GutenbergKit
* [EditorService] so callers can be tested without the real
* service.
*/
interface EditorServiceProvider {
suspend fun prepare(
context: Context,
configuration: EditorConfiguration,
coroutineScope: CoroutineScope
): EditorDependencies
}

@InstallIn(SingletonComponent::class)
@Module
interface EditorServiceProviderModule {
@Binds
fun bindEditorServiceProvider(impl: EditorServiceProviderImpl): EditorServiceProvider
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.wordpress.android.ui.posts

import android.content.Context
import kotlinx.coroutines.CoroutineScope
import org.wordpress.gutenberg.model.EditorConfiguration
import org.wordpress.gutenberg.model.EditorDependencies
import org.wordpress.gutenberg.services.EditorService
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class EditorServiceProviderImpl @Inject constructor() :
EditorServiceProvider {
override suspend fun prepare(
context: Context,
configuration: EditorConfiguration,
coroutineScope: CoroutineScope
): EditorDependencies {
val service = EditorService.create(
context = context,
configuration = configuration,
coroutineScope = coroutineScope
)
return service.prepare(null)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package org.wordpress.android.ui.posts

import android.content.Context
import androidx.annotation.MainThread
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.wordpress.android.datasets.SiteSettingsProvider
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.store.AccountStore
import org.wordpress.android.modules.BG_THREAD
import org.wordpress.android.repositories.EditorSettingsRepository
import org.wordpress.android.util.AppLog
import org.wordpress.gutenberg.model.EditorDependencies
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton

/**
* Opportunistically preloads GutenbergKit editor dependencies in the
* background so the editor opens faster.
*
* Cached dependencies are keyed by site local ID, so switching
* between sites does not discard previously preloaded results.
*
* ## Usage
*
* - [preloadIfNeeded] — idempotent; call whenever a site becomes
* visible. Skips work if the site was already preloaded or a job
* is in flight.
* - [refreshPreloading] — discards the cached result for a site
* and re-preloads from scratch (e.g. on pull-to-refresh).
* - [getDependencies] — returns the cached result for a site, or
* `null` if preloading has not completed. Callers must handle
* `null` gracefully by loading dependencies themselves.
* - [clear] — cancels all in-flight work and releases all cached
* data. Call when the driving scope is being destroyed.
*
* ## Threading
*
* Public methods are annotated [@MainThread] and must only be
* called from the main thread. [state] is a [ConcurrentHashMap],
* so the background coroutine can safely write [Ready] or remove
* entries without thread-hopping.
*
* ## Deduplication
*
* Preloading is skipped when the site already has a cached result
* or an in-flight job. On failure the entry is removed so the
* next visit retries automatically. If a caller's coroutine scope
* is cancelled externally, [shouldPreload] detects the dead
* [Loading] entry and allows a fresh attempt.
*/
@Singleton
class GutenbergEditorPreloader @Inject constructor(
@ApplicationContext private val appContext: Context,
private val accountStore: AccountStore,
private val gutenbergKitFeatureChecker: GutenbergKitFeatureChecker,
private val gutenbergKitSettingsBuilder: GutenbergKitSettingsBuilder,
private val siteSettingsProvider: SiteSettingsProvider,
private val editorServiceProvider: EditorServiceProvider,
private val editorSettingsRepository: EditorSettingsRepository,
@Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher
) {
private sealed class PreloadState {
data class Loading(val job: Job) : PreloadState()
data class Ready(
val dependencies: EditorDependencies
) : PreloadState()
}

private val state = ConcurrentHashMap<Int, PreloadState>()

/**
* Starts a background preload for [site] if one hasn't already
* been performed for this site and no job is currently in
* flight for it.
*
* [scope] is the caller's [CoroutineScope] (typically
* `viewModelScope`); the launched coroutine is cancelled when
* that scope is cancelled.
*/
@MainThread
fun preloadIfNeeded(site: SiteModel, scope: CoroutineScope) {
if (!shouldPreload(site)) return

val siteId = site.id
val job = scope.launch(bgDispatcher) {
try {
editorSettingsRepository
.fetchEditorCapabilitiesForSite(site)
val config = gutenbergKitSettingsBuilder
.buildPostConfiguration(
site = site,
accessToken = accountStore.accessToken
)
val result = editorServiceProvider.prepare(
context = appContext,
configuration = config,
coroutineScope = scope
)
state[siteId] = PreloadState.Ready(result)
AppLog.d(
AppLog.T.EDITOR,
"Editor dependencies preloaded for" +
" site ${site.name}"
)
} catch (
@Suppress("TooGenericExceptionCaught") e: Exception
) {
AppLog.e(
AppLog.T.EDITOR,
"Failed to preload editor dependencies",
e
)
state.remove(siteId)
}
}
state[siteId] = PreloadState.Loading(job)
}

/**
* Discards any cached result for [site] and re-preloads from
* scratch. Use for pull-to-refresh or any scenario where the
* caller wants to force a fresh fetch.
*/
@MainThread
fun refreshPreloading(site: SiteModel, scope: CoroutineScope) {
clearSite(site)
preloadIfNeeded(site, scope)
}

/**
* Returns the preloaded dependencies for [site], or `null` if
* preloading has not completed (or failed). Callers must handle
* `null` gracefully by loading dependencies themselves.
*/
@MainThread
fun getDependencies(site: SiteModel): EditorDependencies? =
getDependencies(site.id)

@MainThread
fun getDependencies(siteLocalId: Int): EditorDependencies? =
(state[siteLocalId] as? PreloadState.Ready)?.dependencies

/**
* Cancels all in-flight preloads and discards all cached
* results. Call when the driving scope is being destroyed.
*/
@MainThread
fun clear() {
state.values.forEach { entry ->
if (entry is PreloadState.Loading) entry.job.cancel()
}
state.clear()
}

private fun clearSite(site: SiteModel) {
val entry = state.remove(site.id)
if (entry is PreloadState.Loading) entry.job.cancel()
}

private fun shouldPreload(site: SiteModel): Boolean {
val isEnabled =
gutenbergKitFeatureChecker.isGutenbergKitEnabled() &&
siteSettingsProvider.isBlockEditorDefault(site)
val isAlreadyHandled = when (val entry = state[site.id]) {
is PreloadState.Loading -> entry.job.isActive
is PreloadState.Ready -> true
null -> false
}
return isEnabled && !isAlreadyHandled
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2210,7 +2210,10 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene
val post = editPostRepository.getPost()
val configuration = buildEditorConfiguration(siteModel, post)

return GutenbergKitEditorFragment.newInstance(configuration)
return GutenbergKitEditorFragment.newInstance(
configuration,
siteModel
)
}

private fun buildEditorConfiguration(
Expand Down
Loading
Loading