Skip to content

Commit 146f2cb

Browse files
committed
Merge remote-tracking branch 'origin/trunk' into android-asset-caching
2 parents ca8a8da + 450d790 commit 146f2cb

File tree

17 files changed

+571
-27
lines changed

17 files changed

+571
-27
lines changed

android/Gutenberg/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ android {
1414
}
1515

1616
defaultConfig {
17-
minSdk = 22
17+
minSdk = 24
1818

1919
buildConfigField(
2020
"String",

android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,7 @@ class GutenbergView : WebView {
563563
filePathCallback = null
564564
onFileChooserRequested = null
565565
handler.removeCallbacksAndMessages(null)
566+
this.destroy()
566567
}
567568

568569
companion object {

android/app/build.gradle.kts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ android {
99

1010
defaultConfig {
1111
applicationId = "com.example.gutenbergkit"
12-
minSdk = 22
12+
minSdk = 24
1313
targetSdk = 34
1414
versionCode = 1
1515
versionName = "1.0"
@@ -43,8 +43,10 @@ dependencies {
4343
implementation(libs.androidx.activity)
4444
implementation(libs.androidx.constraintlayout)
4545
implementation(libs.androidx.webkit)
46+
implementation(libs.androidx.recyclerview)
47+
implementation(libs.wordpress.rs.android)
4648
implementation(project(":Gutenberg"))
4749
testImplementation(libs.junit)
4850
androidTestImplementation(libs.androidx.junit)
4951
androidTestImplementation(libs.androidx.espresso.core)
50-
}
52+
}

android/app/src/main/AndroidManifest.xml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,27 @@
1515
tools:targetApi="31">
1616
<activity
1717
android:name=".MainActivity"
18-
android:exported="true">
18+
android:exported="true"
19+
android:launchMode="singleTop">
1920
<intent-filter>
2021
<action android:name="android.intent.action.MAIN" />
2122

2223
<category android:name="android.intent.category.LAUNCHER" />
2324
</intent-filter>
25+
26+
<intent-filter>
27+
<action android:name="android.intent.action.VIEW" />
28+
<category android:name="android.intent.category.DEFAULT" />
29+
<category android:name="android.intent.category.BROWSABLE" />
30+
<data
31+
android:host="authorized"
32+
android:scheme="gutenbergkit" >
33+
</data>
34+
</intent-filter>
2435
</activity>
36+
<activity
37+
android:name=".EditorActivity"
38+
android:exported="false" />
2539
</application>
2640
<uses-permission android:name="android.permission.INTERNET"/>
2741
</manifest>
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package com.example.gutenbergkit
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import android.util.Base64
6+
import androidx.appcompat.app.AlertDialog
7+
import androidx.core.net.toUri
8+
import kotlinx.coroutines.CoroutineScope
9+
import kotlinx.coroutines.Dispatchers
10+
import kotlinx.coroutines.launch
11+
import kotlinx.coroutines.withContext
12+
import rs.wordpress.api.kotlin.ApiDiscoveryResult
13+
import rs.wordpress.api.kotlin.WpLoginClient
14+
15+
class AuthenticationManager(private val context: Context) {
16+
interface AuthenticationCallback {
17+
fun onAuthenticationSuccess(siteUrl: String, siteApiRoot: String, authToken: String)
18+
fun onAuthenticationFailure(errorMessage: String)
19+
}
20+
21+
private var currentApiRootUrl: String? = null
22+
23+
fun startAuthentication(siteUrl: String, callback: AuthenticationCallback) {
24+
showProgressDialog { progressDialog ->
25+
CoroutineScope(Dispatchers.IO).launch {
26+
when (val apiDiscoveryResult = WpLoginClient().apiDiscovery(siteUrl)) {
27+
is ApiDiscoveryResult.Success -> {
28+
val success = apiDiscoveryResult.success
29+
val apiRootUrl = success.apiRootUrl.url()
30+
val applicationPasswordAuthenticationUrl =
31+
success.applicationPasswordsAuthenticationUrl.url()
32+
withContext(Dispatchers.Main) {
33+
progressDialog.dismiss()
34+
launchAuthenticationFlow(
35+
apiRootUrl,
36+
applicationPasswordAuthenticationUrl
37+
)
38+
}
39+
}
40+
41+
else -> {
42+
withContext(Dispatchers.Main) {
43+
progressDialog.dismiss()
44+
callback.onAuthenticationFailure("Failed to find api root: $apiDiscoveryResult")
45+
}
46+
}
47+
}
48+
}
49+
}
50+
}
51+
52+
private fun showProgressDialog(onCreated: (AlertDialog) -> Unit) {
53+
val progressView = android.view.LayoutInflater.from(context)
54+
.inflate(android.R.layout.simple_list_item_1, null).apply {
55+
findViewById<android.widget.TextView>(android.R.id.text1).apply {
56+
text = context.getString(R.string.finding_api_root)
57+
gravity = android.view.Gravity.CENTER
58+
setPadding(32, 32, 32, 32)
59+
}
60+
}
61+
62+
val progressDialog = AlertDialog.Builder(context)
63+
.setTitle(context.getString(R.string.discovering_site))
64+
.setView(progressView)
65+
.setCancelable(false)
66+
.create()
67+
.also { it.show() }
68+
69+
onCreated(progressDialog)
70+
}
71+
72+
private fun launchAuthenticationFlow(
73+
apiRootUrl: String,
74+
applicationPasswordAuthenticationUrl: String
75+
) {
76+
// Store the API root URL for use when processing authentication result
77+
currentApiRootUrl = apiRootUrl
78+
79+
val uriBuilder = applicationPasswordAuthenticationUrl.toUri().buildUpon()
80+
81+
uriBuilder
82+
.appendQueryParameter("app_name", "GutenbergKitAndroidDemoApp")
83+
.appendQueryParameter("app_id", "00000000-0000-4000-9000-000000000000")
84+
// Url scheme is defined in AndroidManifest file
85+
.appendQueryParameter("success_url", "gutenbergkit://authorized")
86+
87+
uriBuilder.build().let { uri ->
88+
val intent = Intent(Intent.ACTION_VIEW, uri)
89+
context.startActivity(intent)
90+
}
91+
}
92+
93+
fun processAuthenticationResult(intent: Intent, callback: AuthenticationCallback) {
94+
intent.data?.let { data ->
95+
try {
96+
val siteUrl = data.getQueryParameter("site_url")
97+
?: throw IllegalStateException("site_url is missing from authentication")
98+
val username = data.getQueryParameter("user_login")
99+
?: throw IllegalStateException("username is missing from authentication")
100+
val password = data.getQueryParameter("password")
101+
?: throw IllegalStateException("password is missing from authentication")
102+
103+
val siteApiRoot = currentApiRootUrl
104+
?: throw IllegalStateException("API root URL is not available")
105+
currentApiRootUrl = null
106+
107+
val authToken = "Basic " + Base64.encodeToString(
108+
"$username:$password".toByteArray(),
109+
Base64.NO_WRAP
110+
)
111+
112+
callback.onAuthenticationSuccess(siteUrl, siteApiRoot, authToken)
113+
} catch (e: Exception) {
114+
callback.onAuthenticationFailure("Authentication error: ${e.message}")
115+
}
116+
}
117+
}
118+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.example.gutenbergkit
2+
3+
import android.view.LayoutInflater
4+
import android.view.View
5+
import android.view.ViewGroup
6+
import android.widget.TextView
7+
import androidx.recyclerview.widget.RecyclerView
8+
9+
class ConfigurationAdapter(
10+
private val items: List<ConfigurationItem>,
11+
private val onItemClick: (ConfigurationItem) -> Unit,
12+
private val onItemLongClick: (ConfigurationItem) -> Boolean
13+
) : RecyclerView.Adapter<ConfigurationAdapter.ViewHolder>() {
14+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
15+
val view = LayoutInflater.from(parent.context)
16+
.inflate(R.layout.item_configuration, parent, false)
17+
return ViewHolder(view)
18+
}
19+
20+
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
21+
val item = items[position]
22+
when (item) {
23+
is ConfigurationItem.BundledEditor -> {
24+
holder.titleText.text = holder.itemView.context.getString(R.string.bundled_editor)
25+
holder.subtitleText.text =
26+
holder.itemView.context.getString(R.string.bundled_editor_subtitle)
27+
holder.subtitleText.visibility = View.VISIBLE
28+
}
29+
30+
is ConfigurationItem.RemoteEditor -> {
31+
holder.titleText.text = item.name
32+
holder.subtitleText.text = item.siteUrl
33+
holder.subtitleText.visibility = View.VISIBLE
34+
}
35+
}
36+
37+
holder.itemView.setOnClickListener {
38+
onItemClick(item)
39+
}
40+
41+
holder.itemView.setOnLongClickListener {
42+
onItemLongClick(item)
43+
}
44+
}
45+
46+
override fun getItemCount() = items.size
47+
48+
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
49+
val titleText: TextView = view.findViewById(R.id.titleText)
50+
val subtitleText: TextView = view.findViewById(R.id.subtitleText)
51+
}
52+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.example.gutenbergkit
2+
3+
sealed class ConfigurationItem {
4+
object BundledEditor : ConfigurationItem()
5+
data class RemoteEditor(
6+
val name: String,
7+
val siteUrl: String,
8+
val siteApiRoot: String,
9+
val authHeader: String
10+
) : ConfigurationItem()
11+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.example.gutenbergkit
2+
3+
import android.content.Context
4+
import android.content.SharedPreferences
5+
import androidx.core.content.edit
6+
import org.json.JSONArray
7+
import org.json.JSONObject
8+
9+
class ConfigurationStorage(context: Context) {
10+
private val sharedPrefs: SharedPreferences =
11+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
12+
13+
companion object {
14+
private const val PREFS_NAME = "gutenberg_configs"
15+
private const val KEY_REMOTE_CONFIGS = "remote_configurations"
16+
}
17+
18+
fun saveConfigurations(configurations: List<ConfigurationItem>) {
19+
val jsonArray = JSONArray()
20+
configurations.forEach { config ->
21+
if (config is ConfigurationItem.RemoteEditor) {
22+
val jsonObject = JSONObject().apply {
23+
put("name", config.name)
24+
put("siteUrl", config.siteUrl)
25+
put("siteApiRoot", config.siteApiRoot)
26+
put("authHeader", config.authHeader)
27+
}
28+
jsonArray.put(jsonObject)
29+
}
30+
}
31+
sharedPrefs.edit {
32+
putString(KEY_REMOTE_CONFIGS, jsonArray.toString())
33+
}
34+
}
35+
36+
fun loadConfigurations(): List<ConfigurationItem.RemoteEditor> {
37+
val savedData = sharedPrefs.getString(KEY_REMOTE_CONFIGS, null) ?: return emptyList()
38+
val configurations = mutableListOf<ConfigurationItem.RemoteEditor>()
39+
40+
try {
41+
val jsonArray = JSONArray(savedData)
42+
for (i in 0 until jsonArray.length()) {
43+
val jsonObject = jsonArray.getJSONObject(i)
44+
val config = ConfigurationItem.RemoteEditor(
45+
name = jsonObject.getString("name"),
46+
siteUrl = jsonObject.getString("siteUrl"),
47+
siteApiRoot = jsonObject.optString(
48+
"siteApiRoot",
49+
jsonObject.getString("siteUrl") + "/wp-json/"
50+
),
51+
authHeader = jsonObject.getString("authHeader")
52+
)
53+
configurations.add(config)
54+
}
55+
} catch (e: Exception) {
56+
// Ignore parsing errors
57+
}
58+
59+
return configurations
60+
}
61+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.example.gutenbergkit
2+
3+
import android.os.Bundle
4+
import android.webkit.WebView
5+
import android.content.pm.ApplicationInfo
6+
import androidx.activity.enableEdgeToEdge
7+
import androidx.appcompat.app.AppCompatActivity
8+
import androidx.core.view.ViewCompat
9+
import androidx.core.view.WindowInsetsCompat
10+
import org.wordpress.gutenberg.EditorConfiguration
11+
import org.wordpress.gutenberg.GutenbergView
12+
13+
class EditorActivity : AppCompatActivity() {
14+
override fun onCreate(savedInstanceState: Bundle?) {
15+
super.onCreate(savedInstanceState)
16+
enableEdgeToEdge()
17+
setContentView(R.layout.activity_editor)
18+
19+
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.editor)) { v, insets ->
20+
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
21+
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
22+
insets
23+
}
24+
25+
if (0 != (applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE)) {
26+
WebView.setWebContentsDebuggingEnabled(true)
27+
}
28+
29+
// Get the configuration from the intent
30+
val configuration =
31+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
32+
intent.getParcelableExtra(
33+
MainActivity.EXTRA_CONFIGURATION,
34+
EditorConfiguration::class.java
35+
)
36+
} else {
37+
@Suppress("DEPRECATION")
38+
intent.getParcelableExtra<EditorConfiguration>(MainActivity.EXTRA_CONFIGURATION)
39+
} ?: EditorConfiguration.builder().build()
40+
41+
val gbView = findViewById<GutenbergView>(R.id.gutenbergView)
42+
gbView.start(configuration)
43+
}
44+
}

0 commit comments

Comments
 (0)