diff --git a/android/Gutenberg/build.gradle.kts b/android/Gutenberg/build.gradle.kts
index 45df50310..88a47b276 100644
--- a/android/Gutenberg/build.gradle.kts
+++ b/android/Gutenberg/build.gradle.kts
@@ -14,7 +14,7 @@ android {
}
defaultConfig {
- minSdk = 22
+ minSdk = 24
buildConfigField(
"String",
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 85f71a07e..ec47304f5 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -9,7 +9,7 @@ android {
defaultConfig {
applicationId = "com.example.gutenbergkit"
- minSdk = 22
+ minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
@@ -43,8 +43,10 @@ dependencies {
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.webkit)
+ implementation(libs.androidx.recyclerview)
+ implementation(libs.wordpress.rs.android)
implementation(project(":Gutenberg"))
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
-}
\ No newline at end of file
+}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5f1e529af..810556ebd 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -15,13 +15,27 @@
tools:targetApi="31">
+ android:exported="true"
+ android:launchMode="singleTop">
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/gutenbergkit/AuthenticationManager.kt b/android/app/src/main/java/com/example/gutenbergkit/AuthenticationManager.kt
new file mode 100644
index 000000000..53d28df5e
--- /dev/null
+++ b/android/app/src/main/java/com/example/gutenbergkit/AuthenticationManager.kt
@@ -0,0 +1,118 @@
+package com.example.gutenbergkit
+
+import android.content.Context
+import android.content.Intent
+import android.util.Base64
+import androidx.appcompat.app.AlertDialog
+import androidx.core.net.toUri
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import rs.wordpress.api.kotlin.ApiDiscoveryResult
+import rs.wordpress.api.kotlin.WpLoginClient
+
+class AuthenticationManager(private val context: Context) {
+ interface AuthenticationCallback {
+ fun onAuthenticationSuccess(siteUrl: String, siteApiRoot: String, authToken: String)
+ fun onAuthenticationFailure(errorMessage: String)
+ }
+
+ private var currentApiRootUrl: String? = null
+
+ fun startAuthentication(siteUrl: String, callback: AuthenticationCallback) {
+ showProgressDialog { progressDialog ->
+ CoroutineScope(Dispatchers.IO).launch {
+ when (val apiDiscoveryResult = WpLoginClient().apiDiscovery(siteUrl)) {
+ is ApiDiscoveryResult.Success -> {
+ val success = apiDiscoveryResult.success
+ val apiRootUrl = success.apiRootUrl.url()
+ val applicationPasswordAuthenticationUrl =
+ success.applicationPasswordsAuthenticationUrl.url()
+ withContext(Dispatchers.Main) {
+ progressDialog.dismiss()
+ launchAuthenticationFlow(
+ apiRootUrl,
+ applicationPasswordAuthenticationUrl
+ )
+ }
+ }
+
+ else -> {
+ withContext(Dispatchers.Main) {
+ progressDialog.dismiss()
+ callback.onAuthenticationFailure("Failed to find api root: $apiDiscoveryResult")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun showProgressDialog(onCreated: (AlertDialog) -> Unit) {
+ val progressView = android.view.LayoutInflater.from(context)
+ .inflate(android.R.layout.simple_list_item_1, null).apply {
+ findViewById(android.R.id.text1).apply {
+ text = context.getString(R.string.finding_api_root)
+ gravity = android.view.Gravity.CENTER
+ setPadding(32, 32, 32, 32)
+ }
+ }
+
+ val progressDialog = AlertDialog.Builder(context)
+ .setTitle(context.getString(R.string.discovering_site))
+ .setView(progressView)
+ .setCancelable(false)
+ .create()
+ .also { it.show() }
+
+ onCreated(progressDialog)
+ }
+
+ private fun launchAuthenticationFlow(
+ apiRootUrl: String,
+ applicationPasswordAuthenticationUrl: String
+ ) {
+ // Store the API root URL for use when processing authentication result
+ currentApiRootUrl = apiRootUrl
+
+ val uriBuilder = applicationPasswordAuthenticationUrl.toUri().buildUpon()
+
+ uriBuilder
+ .appendQueryParameter("app_name", "GutenbergKitAndroidDemoApp")
+ .appendQueryParameter("app_id", "00000000-0000-4000-9000-000000000000")
+ // Url scheme is defined in AndroidManifest file
+ .appendQueryParameter("success_url", "gutenbergkit://authorized")
+
+ uriBuilder.build().let { uri ->
+ val intent = Intent(Intent.ACTION_VIEW, uri)
+ context.startActivity(intent)
+ }
+ }
+
+ fun processAuthenticationResult(intent: Intent, callback: AuthenticationCallback) {
+ intent.data?.let { data ->
+ try {
+ val siteUrl = data.getQueryParameter("site_url")
+ ?: throw IllegalStateException("site_url is missing from authentication")
+ val username = data.getQueryParameter("user_login")
+ ?: throw IllegalStateException("username is missing from authentication")
+ val password = data.getQueryParameter("password")
+ ?: throw IllegalStateException("password is missing from authentication")
+
+ val siteApiRoot = currentApiRootUrl
+ ?: throw IllegalStateException("API root URL is not available")
+ currentApiRootUrl = null
+
+ val authToken = "Basic " + Base64.encodeToString(
+ "$username:$password".toByteArray(),
+ Base64.NO_WRAP
+ )
+
+ callback.onAuthenticationSuccess(siteUrl, siteApiRoot, authToken)
+ } catch (e: Exception) {
+ callback.onAuthenticationFailure("Authentication error: ${e.message}")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/gutenbergkit/ConfigurationAdapter.kt b/android/app/src/main/java/com/example/gutenbergkit/ConfigurationAdapter.kt
new file mode 100644
index 000000000..1b053b505
--- /dev/null
+++ b/android/app/src/main/java/com/example/gutenbergkit/ConfigurationAdapter.kt
@@ -0,0 +1,52 @@
+package com.example.gutenbergkit
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+
+class ConfigurationAdapter(
+ private val items: List,
+ private val onItemClick: (ConfigurationItem) -> Unit,
+ private val onItemLongClick: (ConfigurationItem) -> Boolean
+) : RecyclerView.Adapter() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_configuration, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val item = items[position]
+ when (item) {
+ is ConfigurationItem.BundledEditor -> {
+ holder.titleText.text = holder.itemView.context.getString(R.string.bundled_editor)
+ holder.subtitleText.text =
+ holder.itemView.context.getString(R.string.bundled_editor_subtitle)
+ holder.subtitleText.visibility = View.VISIBLE
+ }
+
+ is ConfigurationItem.RemoteEditor -> {
+ holder.titleText.text = item.name
+ holder.subtitleText.text = item.siteUrl
+ holder.subtitleText.visibility = View.VISIBLE
+ }
+ }
+
+ holder.itemView.setOnClickListener {
+ onItemClick(item)
+ }
+
+ holder.itemView.setOnLongClickListener {
+ onItemLongClick(item)
+ }
+ }
+
+ override fun getItemCount() = items.size
+
+ class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val titleText: TextView = view.findViewById(R.id.titleText)
+ val subtitleText: TextView = view.findViewById(R.id.subtitleText)
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/gutenbergkit/ConfigurationItem.kt b/android/app/src/main/java/com/example/gutenbergkit/ConfigurationItem.kt
new file mode 100644
index 000000000..66c993bf0
--- /dev/null
+++ b/android/app/src/main/java/com/example/gutenbergkit/ConfigurationItem.kt
@@ -0,0 +1,11 @@
+package com.example.gutenbergkit
+
+sealed class ConfigurationItem {
+ object BundledEditor : ConfigurationItem()
+ data class RemoteEditor(
+ val name: String,
+ val siteUrl: String,
+ val siteApiRoot: String,
+ val authHeader: String
+ ) : ConfigurationItem()
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/gutenbergkit/ConfigurationStorage.kt b/android/app/src/main/java/com/example/gutenbergkit/ConfigurationStorage.kt
new file mode 100644
index 000000000..02bca946e
--- /dev/null
+++ b/android/app/src/main/java/com/example/gutenbergkit/ConfigurationStorage.kt
@@ -0,0 +1,61 @@
+package com.example.gutenbergkit
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.core.content.edit
+import org.json.JSONArray
+import org.json.JSONObject
+
+class ConfigurationStorage(context: Context) {
+ private val sharedPrefs: SharedPreferences =
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+
+ companion object {
+ private const val PREFS_NAME = "gutenberg_configs"
+ private const val KEY_REMOTE_CONFIGS = "remote_configurations"
+ }
+
+ fun saveConfigurations(configurations: List) {
+ val jsonArray = JSONArray()
+ configurations.forEach { config ->
+ if (config is ConfigurationItem.RemoteEditor) {
+ val jsonObject = JSONObject().apply {
+ put("name", config.name)
+ put("siteUrl", config.siteUrl)
+ put("siteApiRoot", config.siteApiRoot)
+ put("authHeader", config.authHeader)
+ }
+ jsonArray.put(jsonObject)
+ }
+ }
+ sharedPrefs.edit {
+ putString(KEY_REMOTE_CONFIGS, jsonArray.toString())
+ }
+ }
+
+ fun loadConfigurations(): List {
+ val savedData = sharedPrefs.getString(KEY_REMOTE_CONFIGS, null) ?: return emptyList()
+ val configurations = mutableListOf()
+
+ try {
+ val jsonArray = JSONArray(savedData)
+ for (i in 0 until jsonArray.length()) {
+ val jsonObject = jsonArray.getJSONObject(i)
+ val config = ConfigurationItem.RemoteEditor(
+ name = jsonObject.getString("name"),
+ siteUrl = jsonObject.getString("siteUrl"),
+ siteApiRoot = jsonObject.optString(
+ "siteApiRoot",
+ jsonObject.getString("siteUrl") + "/wp-json/"
+ ),
+ authHeader = jsonObject.getString("authHeader")
+ )
+ configurations.add(config)
+ }
+ } catch (e: Exception) {
+ // Ignore parsing errors
+ }
+
+ return configurations
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/gutenbergkit/EditorActivity.kt b/android/app/src/main/java/com/example/gutenbergkit/EditorActivity.kt
new file mode 100644
index 000000000..bd44e3a00
--- /dev/null
+++ b/android/app/src/main/java/com/example/gutenbergkit/EditorActivity.kt
@@ -0,0 +1,44 @@
+package com.example.gutenbergkit
+
+import android.os.Bundle
+import android.webkit.WebView
+import android.content.pm.ApplicationInfo
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import org.wordpress.gutenberg.EditorConfiguration
+import org.wordpress.gutenberg.GutenbergView
+
+class EditorActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContentView(R.layout.activity_editor)
+
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.editor)) { v, insets ->
+ val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
+ insets
+ }
+
+ if (0 != (applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE)) {
+ WebView.setWebContentsDebuggingEnabled(true)
+ }
+
+ // Get the configuration from the intent
+ val configuration =
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableExtra(
+ MainActivity.EXTRA_CONFIGURATION,
+ EditorConfiguration::class.java
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ intent.getParcelableExtra(MainActivity.EXTRA_CONFIGURATION)
+ } ?: EditorConfiguration.builder().build()
+
+ val gbView = findViewById(R.id.gutenbergView)
+ gbView.start(configuration)
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/example/gutenbergkit/MainActivity.kt b/android/app/src/main/java/com/example/gutenbergkit/MainActivity.kt
index 421f5807d..2ba0884a3 100644
--- a/android/app/src/main/java/com/example/gutenbergkit/MainActivity.kt
+++ b/android/app/src/main/java/com/example/gutenbergkit/MainActivity.kt
@@ -1,37 +1,74 @@
package com.example.gutenbergkit
+import android.content.Intent
import android.os.Bundle
-import android.webkit.WebView
-import androidx.activity.enableEdgeToEdge
+import android.widget.EditText
+import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
-import org.wordpress.gutenberg.GutenbergView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.floatingactionbutton.FloatingActionButton
import org.wordpress.gutenberg.EditorConfiguration
-class MainActivity : AppCompatActivity() {
+class MainActivity : AppCompatActivity(), AuthenticationManager.AuthenticationCallback {
+ private lateinit var recyclerView: RecyclerView
+ private lateinit var adapter: ConfigurationAdapter
+ private val configurations = mutableListOf()
+ private lateinit var configurationStorage: ConfigurationStorage
+ private lateinit var authenticationManager: AuthenticationManager
+
+ companion object {
+ const val EXTRA_CONFIGURATION = "configuration"
+ }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- enableEdgeToEdge()
- setContentView(R.layout.activity_main)
- ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
- val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
- v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
- insets
- }
+ setContentView(R.layout.activity_configuration)
- WebView.setWebContentsDebuggingEnabled(true)
+ title = getString(R.string.demo_title)
+ configurationStorage = ConfigurationStorage(this)
+ authenticationManager = AuthenticationManager(this)
- val gbView = findViewById(R.id.gutenbergView)
+ recyclerView = findViewById(R.id.configurationsRecyclerView)
+ recyclerView.layoutManager = LinearLayoutManager(this)
- val config = EditorConfiguration.builder()
- .setTitle("")
- .setContent("")
- .setPostType("post")
- .setThemeStyles(false)
+ // Add default bundled editor configuration
+ configurations.add(ConfigurationItem.BundledEditor)
+
+ // Load saved configurations
+ configurations.addAll(configurationStorage.loadConfigurations())
+
+ adapter = ConfigurationAdapter(
+ configurations,
+ onItemClick = { config ->
+ when (config) {
+ is ConfigurationItem.BundledEditor -> launchEditor(createBundledConfiguration())
+ is ConfigurationItem.RemoteEditor -> {
+ launchEditor(createRemoteConfiguration(config))
+ }
+ }
+ },
+ onItemLongClick = { config ->
+ when (config) {
+ is ConfigurationItem.BundledEditor -> false // Can't delete bundled editor
+ is ConfigurationItem.RemoteEditor -> {
+ showDeleteDialog(config)
+ true
+ }
+ }
+ }
+ )
+ recyclerView.adapter = adapter
+
+ // Add FAB for adding new remote configurations
+ findViewById(R.id.addConfigurationFab).setOnClickListener {
+ showAddConfigurationDialog()
+ }
+ }
+
+ private fun createBundledConfiguration(): EditorConfiguration =
+ createCommonConfigurationBuilder()
.setPlugins(false)
- .setHideTitle(false)
.setSiteURL("")
.setSiteApiRoot("")
.setSiteApiNamespace(arrayOf())
@@ -41,6 +78,88 @@ class MainActivity : AppCompatActivity() {
.setCookies(emptyMap())
.build()
- gbView.start(config)
+ private fun createRemoteConfiguration(config: ConfigurationItem.RemoteEditor): EditorConfiguration =
+ createCommonConfigurationBuilder()
+ .setPlugins(true) // Enable plugins for remote editor
+ .setSiteURL(config.siteUrl)
+ .setSiteApiRoot(config.siteApiRoot)
+ .setSiteApiNamespace(arrayOf("wp/v2"))
+ .setNamespaceExcludedPaths(arrayOf())
+ .setAuthHeader(config.authHeader)
+ .build()
+
+ private fun createCommonConfigurationBuilder(): EditorConfiguration.Builder =
+ EditorConfiguration.builder()
+ .setTitle("")
+ .setContent("")
+ .setPostType("post")
+ .setThemeStyles(false)
+ .setHideTitle(false)
+ .setWebViewGlobals(emptyList())
+ .setCookies(emptyMap())
+
+ private fun launchEditor(configuration: EditorConfiguration) {
+ val intent = Intent(this, EditorActivity::class.java)
+ intent.putExtra(EXTRA_CONFIGURATION, configuration)
+ startActivity(intent)
+ }
+
+ private fun showAddConfigurationDialog() {
+ val dialogView = layoutInflater.inflate(R.layout.dialog_configuration, null)
+ val siteUrlInput = dialogView.findViewById(R.id.siteUrlInput)
+
+ AlertDialog.Builder(this)
+ .setTitle(getString(R.string.add_remote_configuration))
+ .setView(dialogView)
+ .setPositiveButton(getString(R.string.add)) { dialog, _ ->
+ val siteUrl = siteUrlInput.text.toString().trim()
+ if (siteUrl.isNotEmpty()) {
+ dialog.dismiss()
+ authenticationManager.startAuthentication(siteUrl, this)
+ }
+ }
+ .setNegativeButton(getString(R.string.cancel), null)
+ .show()
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ authenticationManager.processAuthenticationResult(intent, this)
+ }
+
+ override fun onAuthenticationSuccess(siteUrl: String, siteApiRoot: String, authToken: String) {
+ val siteName = siteUrl.removePrefix("https://").removePrefix("http://").substringBefore("/")
+ val newConfig = ConfigurationItem.RemoteEditor(
+ name = siteName,
+ siteUrl = siteUrl,
+ siteApiRoot = siteApiRoot,
+ authHeader = authToken
+ )
+ configurations.add(newConfig)
+ adapter.notifyItemInserted(configurations.size - 1)
+ configurationStorage.saveConfigurations(configurations)
+ }
+
+ override fun onAuthenticationFailure(errorMessage: String) {
+ AlertDialog.Builder(this)
+ .setTitle(getString(R.string.authentication_failed))
+ .setMessage(errorMessage)
+ .setPositiveButton(getString(R.string.ok), null)
+ .setCancelable(true)
+ .show()
+ }
+
+ private fun showDeleteDialog(config: ConfigurationItem.RemoteEditor) {
+ AlertDialog.Builder(this)
+ .setTitle(getString(R.string.delete_site_title))
+ .setMessage(getString(R.string.delete_site_message))
+ .setPositiveButton(getString(R.string.delete)) { _, _ ->
+ val index = configurations.indexOf(config)
+ configurations.removeAt(index)
+ adapter.notifyItemRemoved(index)
+ configurationStorage.saveConfigurations(configurations)
+ }
+ .setNegativeButton(getString(R.string.cancel), null)
+ .show()
}
-}
+}
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/activity_configuration.xml b/android/app/src/main/res/layout/activity_configuration.xml
new file mode 100644
index 000000000..5e2da2270
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_configuration.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/activity_editor.xml b/android/app/src/main/res/layout/activity_editor.xml
new file mode 100644
index 000000000..c39ed4db4
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_editor.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/dialog_configuration.xml b/android/app/src/main/res/layout/dialog_configuration.xml
new file mode 100644
index 000000000..e4eb918f8
--- /dev/null
+++ b/android/app/src/main/res/layout/dialog_configuration.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/item_configuration.xml b/android/app/src/main/res/layout/item_configuration.xml
new file mode 100644
index 000000000..1662e8932
--- /dev/null
+++ b/android/app/src/main/res/layout/item_configuration.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 9ed8d8ddd..a29b2d1b2 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -1,3 +1,27 @@
GutenbergKit
+
+
+ GutenbergKit Demo
+ Bundled Editor
+ Offline editor with bundled assets
+ Add remote configuration
+
+
+ Add Remote Configuration
+ Site URL
+ Add
+ Cancel
+ Delete
+ Delete Site
+ Are you sure you want to delete this site configuration?
+
+
+ Authentication Failed
+ OK
+ Discovering Site
+ Finding API root and authentication URL...
+
+
+ Editor
\ No newline at end of file
diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml
index 7be119596..3165d57f5 100644
--- a/android/gradle/libs.versions.toml
+++ b/android/gradle/libs.versions.toml
@@ -1,6 +1,6 @@
[versions]
agp = "8.7.3"
-kotlin = "1.9.0"
+kotlin = "2.0.21"
coreKtx = "1.13.1"
junit = "4.13.2"
junitVersion = "1.2.1"
@@ -13,6 +13,9 @@ webkit = "1.11.0"
gson = "2.8.9"
mockito = "4.1.0"
robolectric = "4.14.1"
+kotlinx-coroutines = '1.10.2'
+androidx-recyclerview = '1.3.2'
+wordpress-rs = 'trunk-503f1da9e067677d1517d09f926b1d038dfa58a1'
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -28,6 +31,9 @@ gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" }
mockito-kotlin = { group = "org.mockito.kotlin", name = "mockito-kotlin", version.ref = "mockito" }
robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" }
+kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
+androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "androidx-recyclerview" }
+wordpress-rs-android = { group = "rs.wordpress.api", name = "android", version.ref = "wordpress-rs" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
index 167584105..db039cbee 100644
--- a/android/settings.gradle.kts
+++ b/android/settings.gradle.kts
@@ -28,6 +28,12 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
+ maven {
+ url = uri("https://a8c-libs.s3.amazonaws.com/android")
+ content {
+ includeGroup("rs.wordpress.api")
+ }
+ }
}
}