Skip to content

Commit 07124da

Browse files
authored
feat: Discover Android demo app configuration (#225)
* task: Add site discovery to Android demo app Mirror the iOS demo app features. * refactor: Rely upon wordpress-rs * refactor: Prefer `lifecycleScope` over `CoroutineScope` Follow best practices to avoid memory leaks.
1 parent a3a4b08 commit 07124da

File tree

3 files changed

+135
-12
lines changed

3 files changed

+135
-12
lines changed

android/app/src/main/java/com/example/gutenbergkit/MainActivity.kt

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,18 @@ import com.example.gutenbergkit.ui.dialogs.AddConfigurationDialog
4242
import com.example.gutenbergkit.ui.dialogs.DeleteConfigurationDialog
4343
import com.example.gutenbergkit.ui.dialogs.DiscoveringSiteDialog
4444
import com.example.gutenbergkit.ui.theme.AppTheme
45+
import androidx.lifecycle.lifecycleScope
46+
import kotlinx.coroutines.launch
4547
import org.wordpress.gutenberg.BuildConfig
4648
import org.wordpress.gutenberg.EditorConfiguration
4749

4850
class MainActivity : ComponentActivity(), AuthenticationManager.AuthenticationCallback {
4951
private val configurations = mutableStateListOf<ConfigurationItem>()
5052
private val isDiscoveringSite = mutableStateOf(false)
53+
private val isLoadingCapabilities = mutableStateOf(false)
5154
private lateinit var configurationStorage: ConfigurationStorage
5255
private lateinit var authenticationManager: AuthenticationManager
56+
private val siteCapabilitiesDiscovery = SiteCapabilitiesDiscovery()
5357

5458
companion object {
5559
const val EXTRA_CONFIGURATION = "configuration"
@@ -75,7 +79,7 @@ class MainActivity : ComponentActivity(), AuthenticationManager.AuthenticationCa
7579
onConfigurationClick = { config ->
7680
when (config) {
7781
is ConfigurationItem.BundledEditor -> launchEditor(createBundledConfiguration())
78-
is ConfigurationItem.RemoteEditor -> launchEditor(createRemoteConfiguration(config))
82+
is ConfigurationItem.RemoteEditor -> loadRemoteEditor(config)
7983
}
8084
},
8185
onConfigurationLongClick = { config ->
@@ -93,7 +97,8 @@ class MainActivity : ComponentActivity(), AuthenticationManager.AuthenticationCa
9397
configurationStorage.saveConfigurations(configurations)
9498
},
9599
isDiscoveringSite = isDiscoveringSite.value,
96-
onDismissDiscovering = { isDiscoveringSite.value = false }
100+
onDismissDiscovering = { isDiscoveringSite.value = false },
101+
isLoadingCapabilities = isLoadingCapabilities.value
97102
)
98103
}
99104
}
@@ -110,14 +115,42 @@ class MainActivity : ComponentActivity(), AuthenticationManager.AuthenticationCa
110115
.setCookies(emptyMap())
111116
.build()
112117

113-
private fun createRemoteConfiguration(config: ConfigurationItem.RemoteEditor): EditorConfiguration =
114-
createCommonConfigurationBuilder()
115-
.setPlugins(true)
116-
.setSiteURL(config.siteUrl)
117-
.setSiteApiRoot(config.siteApiRoot)
118-
.setNamespaceExcludedPaths(arrayOf())
119-
.setAuthHeader(config.authHeader)
120-
.build()
118+
private fun loadRemoteEditor(config: ConfigurationItem.RemoteEditor) {
119+
isLoadingCapabilities.value = true
120+
121+
lifecycleScope.launch {
122+
try {
123+
val capabilities = siteCapabilitiesDiscovery.discoverCapabilities(
124+
siteApiRoot = config.siteApiRoot
125+
)
126+
127+
val editorConfiguration = createCommonConfigurationBuilder()
128+
.setPlugins(capabilities.supportsPlugins)
129+
.setThemeStyles(capabilities.supportsThemeStyles)
130+
.setSiteURL(config.siteUrl)
131+
.setSiteApiRoot(config.siteApiRoot)
132+
.setNamespaceExcludedPaths(arrayOf())
133+
.setAuthHeader(config.authHeader)
134+
.build()
135+
136+
isLoadingCapabilities.value = false
137+
launchEditor(editorConfiguration)
138+
} catch (e: Exception) {
139+
isLoadingCapabilities.value = false
140+
// If capability discovery fails, use default configuration
141+
val defaultConfiguration = createCommonConfigurationBuilder()
142+
.setPlugins(false)
143+
.setThemeStyles(false)
144+
.setSiteURL(config.siteUrl)
145+
.setSiteApiRoot(config.siteApiRoot)
146+
.setNamespaceExcludedPaths(arrayOf())
147+
.setAuthHeader(config.authHeader)
148+
.build()
149+
150+
launchEditor(defaultConfiguration)
151+
}
152+
}
153+
}
121154

122155
private fun createCommonConfigurationBuilder(): EditorConfiguration.Builder =
123156
EditorConfiguration.builder()
@@ -171,7 +204,8 @@ fun MainScreen(
171204
onAddConfiguration: (String) -> Unit,
172205
onDeleteConfiguration: (ConfigurationItem) -> Unit,
173206
isDiscoveringSite: Boolean = false,
174-
onDismissDiscovering: () -> Unit = {}
207+
onDismissDiscovering: () -> Unit = {},
208+
isLoadingCapabilities: Boolean = false
175209
) {
176210
var showAddDialog = remember { mutableStateOf(false) }
177211
var showDeleteDialog = remember { mutableStateOf<ConfigurationItem.RemoteEditor?>(null) }
@@ -295,6 +329,12 @@ fun MainScreen(
295329
onDismiss = onDismissDiscovering
296330
)
297331
}
332+
333+
if (isLoadingCapabilities) {
334+
DiscoveringSiteDialog(
335+
onDismiss = { /* Cannot dismiss while loading */ }
336+
)
337+
}
298338
}
299339

300340
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.example.gutenbergkit
2+
3+
import android.util.Log
4+
import kotlinx.coroutines.Dispatchers
5+
import kotlinx.coroutines.withContext
6+
import rs.wordpress.api.kotlin.ApiDiscoveryResult
7+
import rs.wordpress.api.kotlin.WpLoginClient
8+
9+
/**
10+
* Data class representing the capabilities discovered from a WordPress site.
11+
*/
12+
data class SiteCapabilities(
13+
val supportsPlugins: Boolean,
14+
val supportsThemeStyles: Boolean
15+
)
16+
17+
/**
18+
* Discovers WordPress site capabilities by querying the API root endpoint.
19+
* This mirrors the iOS implementation's capability discovery logic.
20+
*/
21+
class SiteCapabilitiesDiscovery {
22+
23+
companion object {
24+
private const val TAG = "SiteCapabilitiesDiscovery"
25+
26+
// Routes to check for capability support
27+
private const val ROUTE_EDITOR_ASSETS = "/wpcom/v2/editor-assets"
28+
private const val ROUTE_EDITOR_SETTINGS = "/wp-block-editor/v1/settings"
29+
}
30+
31+
/**
32+
* Discovers site capabilities via API discovery.
33+
*
34+
* @param siteApiRoot The WordPress REST API root URL (e.g., "https://example.com/wp-json")
35+
* @return SiteCapabilities indicating which features are supported
36+
*/
37+
suspend fun discoverCapabilities(
38+
siteApiRoot: String,
39+
): SiteCapabilities = withContext(Dispatchers.IO) {
40+
try {
41+
// Extract the site URL from the API root URL
42+
// e.g., "https://example.com/wp-json" -> "https://example.com"
43+
val siteUrl = siteApiRoot.removeSuffix("/").substringBeforeLast("/wp-json")
44+
45+
// Use WpLoginClient to perform API discovery, which includes API details
46+
when (val apiDiscoveryResult = WpLoginClient().apiDiscovery(siteUrl)) {
47+
is ApiDiscoveryResult.Success -> {
48+
val success = apiDiscoveryResult.success
49+
val apiDetails = success.apiDetails
50+
51+
// Check if the site has the required routes using hasRoute() method
52+
val supportsPlugins = apiDetails.hasRoute(ROUTE_EDITOR_ASSETS)
53+
val supportsThemeStyles = apiDetails.hasRoute(ROUTE_EDITOR_SETTINGS)
54+
55+
Log.d(TAG, "Discovered capabilities - Plugins: $supportsPlugins, Theme Styles: $supportsThemeStyles")
56+
57+
SiteCapabilities(
58+
supportsPlugins = supportsPlugins,
59+
supportsThemeStyles = supportsThemeStyles
60+
)
61+
}
62+
else -> {
63+
Log.w(TAG, "API discovery failed: $apiDiscoveryResult")
64+
getDefaultCapabilities()
65+
}
66+
}
67+
} catch (e: Exception) {
68+
Log.e(TAG, "Error discovering site capabilities", e)
69+
getDefaultCapabilities()
70+
}
71+
}
72+
73+
/**
74+
* Returns default capabilities when discovery fails.
75+
* Conservative defaults: no plugins, no theme styles.
76+
*/
77+
private fun getDefaultCapabilities(): SiteCapabilities {
78+
return SiteCapabilities(
79+
supportsPlugins = false,
80+
supportsThemeStyles = false
81+
)
82+
}
83+
}

android/gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ mockito = "4.1.0"
1515
robolectric = "4.14.1"
1616
kotlinx-coroutines = '1.10.2'
1717
androidx-recyclerview = '1.3.2'
18-
wordpress-rs = 'trunk-503f1da9e067677d1517d09f926b1d038dfa58a1'
18+
wordpress-rs = 'trunk-d02efa6d4d56bc5b44dd2191e837163f9fa27095'
1919
composeBom = "2024.12.01"
2020
activityCompose = "1.9.3"
2121

0 commit comments

Comments
 (0)