Skip to content

Commit 6b1d7c2

Browse files
authored
fix: Android presents file picker for File block upload (#128)
* fix: Android presents file picker for File block upload The previous implementation failed to display a file picker due to the `acceptedTypes` parameter being `[""]`. * refactor: Simplify Android file picker implementation * refactor: Simplify conditional MIME type logic * build: Add mokito and robolectric libraries Expand testing capabilities. * test: Assert Android GutebergView file picker logic
1 parent 3975307 commit 6b1d7c2

File tree

4 files changed

+164
-9
lines changed

4 files changed

+164
-9
lines changed

android/Gutenberg/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ dependencies {
5959
implementation(libs.gson)
6060

6161
testImplementation(libs.junit)
62+
testImplementation(libs.mockito.core)
63+
testImplementation(libs.mockito.kotlin)
64+
testImplementation(libs.robolectric)
6265
androidTestImplementation(libs.androidx.junit)
6366
androidTestImplementation(libs.androidx.espresso.core)
6467
}

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -209,15 +209,13 @@ class GutenbergView : WebView {
209209
): Boolean {
210210
filePathCallback = newFilePathCallback
211211
val allowMultiple = fileChooserParams?.mode == FileChooserParams.MODE_OPEN_MULTIPLE
212-
val mimeTypes = fileChooserParams?.acceptTypes
212+
// Only use `acceptTypes` if it is not merely an empty string
213+
val mimeTypes = fileChooserParams?.acceptTypes?.takeUnless { it.size == 1 && it[0].isEmpty() } ?: arrayOf("*/*")
213214

214-
val intent = Intent(Intent.ACTION_PICK).apply {
215-
type = "*/*" // Default to all types
216-
}
217-
218-
if (!mimeTypes.isNullOrEmpty()) {
219-
intent.type = mimeTypes.joinToString("|")
220-
}
215+
val intent = Intent(Intent.ACTION_GET_CONTENT)
216+
intent.setType(mimeTypes[0])
217+
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
218+
intent.addCategory(Intent.CATEGORY_OPENABLE)
221219

222220
if (allowMultiple) {
223221
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package org.wordpress.gutenberg
2+
3+
import android.content.Intent
4+
import android.net.Uri
5+
import android.os.Looper
6+
import android.webkit.ValueCallback
7+
import android.webkit.WebChromeClient
8+
import android.webkit.WebView
9+
import org.junit.Before
10+
import org.junit.Test
11+
import org.junit.runner.RunWith
12+
import org.mockito.Mock
13+
import org.mockito.Mockito.`when`
14+
import org.mockito.MockitoAnnotations
15+
import org.robolectric.RobolectricTestRunner
16+
import org.robolectric.RuntimeEnvironment
17+
import org.robolectric.Shadows.shadowOf
18+
import org.robolectric.annotation.Config
19+
import org.junit.Assert.assertEquals
20+
import org.junit.Assert.assertTrue
21+
import java.util.concurrent.CountDownLatch
22+
import java.util.concurrent.TimeUnit
23+
24+
@RunWith(RobolectricTestRunner::class)
25+
@Config(sdk = [28], manifest = Config.NONE)
26+
class GutenbergViewTest {
27+
@Mock
28+
private lateinit var mockWebView: WebView
29+
30+
@Mock
31+
private lateinit var mockFilePathCallback: ValueCallback<Array<Uri?>?>
32+
33+
@Mock
34+
private lateinit var mockFileChooserParams: WebChromeClient.FileChooserParams
35+
36+
private lateinit var gutenbergView: GutenbergView
37+
38+
@Before
39+
fun setup() {
40+
MockitoAnnotations.openMocks(this)
41+
gutenbergView = GutenbergView(RuntimeEnvironment.getApplication())
42+
gutenbergView.initializeWebView()
43+
}
44+
45+
@Test
46+
fun `onShowFileChooser sets up file chooser with single file selection`() {
47+
// Given
48+
val latch = CountDownLatch(1)
49+
var capturedIntent: Intent? = null
50+
51+
gutenbergView.setOnFileChooserRequestedListener { intent, _ ->
52+
capturedIntent = intent
53+
latch.countDown()
54+
}
55+
56+
// When
57+
gutenbergView.webChromeClient?.onShowFileChooser(
58+
mockWebView,
59+
mockFilePathCallback,
60+
mockFileChooserParams
61+
)
62+
63+
// Process any pending runnables
64+
shadowOf(Looper.getMainLooper()).runToEndOfTasks()
65+
66+
// Wait for the callback to be executed
67+
latch.await(1, TimeUnit.SECONDS)
68+
69+
// Then
70+
assertTrue("Intent should not be null", capturedIntent != null)
71+
assertTrue("Intent should be a chooser", capturedIntent?.action == Intent.ACTION_CHOOSER)
72+
73+
// Get the original intent from the chooser
74+
val originalIntent = capturedIntent?.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
75+
assertTrue("Original intent should not be null", originalIntent != null)
76+
assertTrue("Original intent action should be ACTION_GET_CONTENT",
77+
originalIntent?.action == Intent.ACTION_GET_CONTENT)
78+
assertTrue("Original intent should have CATEGORY_OPENABLE",
79+
originalIntent?.hasCategory(Intent.CATEGORY_OPENABLE) == true)
80+
assertEquals("Pick image request code should be 1",
81+
1, gutenbergView.pickImageRequestCode)
82+
}
83+
84+
@Test
85+
fun `onShowFileChooser sets up file chooser with multiple file selection`() {
86+
// Given
87+
val latch = CountDownLatch(1)
88+
var capturedIntent: Intent? = null
89+
90+
gutenbergView.setOnFileChooserRequestedListener { intent, _ ->
91+
capturedIntent = intent
92+
latch.countDown()
93+
}
94+
95+
// When
96+
`when`(mockFileChooserParams.mode).thenReturn(WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE)
97+
gutenbergView.webChromeClient?.onShowFileChooser(
98+
mockWebView,
99+
mockFilePathCallback,
100+
mockFileChooserParams
101+
)
102+
103+
// Process any pending runnables
104+
shadowOf(Looper.getMainLooper()).runToEndOfTasks()
105+
106+
// Wait for the callback to be executed
107+
latch.await(1, TimeUnit.SECONDS)
108+
109+
// Then
110+
assertTrue("Intent should not be null", capturedIntent != null)
111+
assertTrue("Intent should be a chooser", capturedIntent?.action == Intent.ACTION_CHOOSER)
112+
113+
// Get the original intent from the chooser
114+
val originalIntent = capturedIntent?.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
115+
assertTrue("Original intent should not be null", originalIntent != null)
116+
assertTrue("Original intent should allow multiple selection",
117+
originalIntent?.getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, false) == true)
118+
}
119+
120+
@Test
121+
fun `onShowFileChooser stores file path callback`() {
122+
// When
123+
gutenbergView.webChromeClient?.onShowFileChooser(
124+
mockWebView,
125+
mockFilePathCallback,
126+
mockFileChooserParams
127+
)
128+
129+
// Then
130+
assertEquals("File path callback should be stored",
131+
mockFilePathCallback, gutenbergView.filePathCallback)
132+
}
133+
134+
@Test
135+
fun `resetFilePathCallback clears the callback`() {
136+
// Given
137+
gutenbergView.webChromeClient?.onShowFileChooser(
138+
mockWebView,
139+
mockFilePathCallback,
140+
mockFileChooserParams
141+
)
142+
143+
// When
144+
gutenbergView.resetFilePathCallback()
145+
146+
// Then
147+
assertEquals("File path callback should be null after reset",
148+
null, gutenbergView.filePathCallback)
149+
}
150+
}

android/gradle/libs.versions.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ activity = "1.9.0"
1111
constraintlayout = "2.1.4"
1212
webkit = "1.11.0"
1313
gson = "2.8.9"
14+
mockito = "4.1.0"
15+
robolectric = "4.14.1"
1416

1517
[libraries]
1618
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -23,9 +25,11 @@ androidx-activity = { group = "androidx.activity", name = "activity", version.re
2325
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
2426
androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref = "webkit" }
2527
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
28+
mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" }
29+
mockito-kotlin = { group = "org.mockito.kotlin", name = "mockito-kotlin", version.ref = "mockito" }
30+
robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" }
2631

2732
[plugins]
2833
android-application = { id = "com.android.application", version.ref = "agp" }
2934
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
3035
android-library = { id = "com.android.library", version.ref = "agp" }
31-

0 commit comments

Comments
 (0)