-
Notifications
You must be signed in to change notification settings - Fork 3
Add multi-site support to Android demo app #155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
065cf16
Implement a basic site selector in Android demo app
oguzkocer 1c18067
Add storage and tap & hold to delete for site configurations in Andro…
oguzkocer 394bb30
Remove `showEditConfigurationDialog`
oguzkocer 80d4ecb
Move androidx.recyclerview dependency to libs.versions.toml
oguzkocer d1557aa
Update Kotlin to 2.0.21
oguzkocer 9496274
Add wordpress-rs as a dependency
oguzkocer de6a6e1
Mock authentication flow implementation for Android demo app
oguzkocer 4833412
Implement authentication flow in Android demo app
oguzkocer e335af1
Show a progress dialog during auto discovery in Android demo
oguzkocer dbe11c8
Split MainActivity in Android demo app
oguzkocer 6ae54c9
Potential fix for code scanning alert no. 415: Android Webview debugg…
oguzkocer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,7 @@ android { | |
| } | ||
|
|
||
| defaultConfig { | ||
| minSdk = 22 | ||
| minSdk = 24 | ||
|
|
||
| buildConfigField( | ||
| "String", | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 118 additions & 0 deletions
118
android/app/src/main/java/com/example/gutenbergkit/AuthenticationManager.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.widget.TextView>(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}") | ||
| } | ||
| } | ||
| } | ||
| } |
52 changes: 52 additions & 0 deletions
52
android/app/src/main/java/com/example/gutenbergkit/ConfigurationAdapter.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<ConfigurationItem>, | ||
| private val onItemClick: (ConfigurationItem) -> Unit, | ||
| private val onItemLongClick: (ConfigurationItem) -> Boolean | ||
| ) : RecyclerView.Adapter<ConfigurationAdapter.ViewHolder>() { | ||
| 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) | ||
| } | ||
| } |
11 changes: 11 additions & 0 deletions
11
android/app/src/main/java/com/example/gutenbergkit/ConfigurationItem.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
| } |
61 changes: 61 additions & 0 deletions
61
android/app/src/main/java/com/example/gutenbergkit/ConfigurationStorage.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<ConfigurationItem>) { | ||
| 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<ConfigurationItem.RemoteEditor> { | ||
| val savedData = sharedPrefs.getString(KEY_REMOTE_CONFIGS, null) ?: return emptyList() | ||
| val configurations = mutableListOf<ConfigurationItem.RemoteEditor>() | ||
|
|
||
| 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 | ||
| } | ||
| } |
44 changes: 44 additions & 0 deletions
44
android/app/src/main/java/com/example/gutenbergkit/EditorActivity.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<EditorConfiguration>(MainActivity.EXTRA_CONFIGURATION) | ||
| } ?: EditorConfiguration.builder().build() | ||
|
|
||
| val gbView = findViewById<GutenbergView>(R.id.gutenbergView) | ||
| gbView.start(configuration) | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that bumping to 24 broke loading the development server from an IP address on the development machine, that it may include tighter default security.
It seems that one now has to add an explicit entry for the IP in the network security configuration. Is there a better way to allow the development server to load?
GutenbergKit/android/app/src/main/res/xml/network_security_config.xml
Lines 3 to 6 in 6ae54c9
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not something I am familiar with :( I've made this bump because
wordpress-rsrequires it, although I don't remember why.Unless this is a major issue for you, I think it's fine to go with a temporary solution like the one you mentioned above. If it's major, I can look into it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While annoying, it's not a blocker for my workflow; I can work around it.
The bigger concern is someone less familiar with the project encountering failed development server loads without knowledge of this workaround. Also, we don't want to commit one-off IP addresses to this configuration file. I'll try to find a solution soon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, please let me know before spending any significant time on this. I'll double check that we need
minSdk = 24forwordpress-rs.If you want, we can remove
wordpress-rs, but I find having a proper authentication flow easier to work with than always having to figure out the authentication token, updating a config document etc. Having said that, I don't plan to work in this repo too much, so whatever works best for you is fine with me.