There is an app where the Users Onboarding Flow is implemented as a Web page and it is being shown in the embedded WebView. One of that flow steps is a file uploading.
It is implemented by using <input/>
tag on the web page and further handling it by an overridden WebChromeClient.onShowFileChooser(..., filePathCallback: ValueCallback<Array<Uri>>?, ...)
which eventually triggers startActivityForResult(...)
which is supposed to start provided by the platform File-Chooser-Activity (can be different for different vendors/version).
And as usual, everything works fine until the activity is being destroyed :)
The problem is when the File-Chooser-Activity is started on top of the app's activity the last one can be destroyed (by the system due the lack of resources, rotation, ... ). Then the WebView whose filePathCallback
we have will be destroyed too. This means the app does not have at this point the valid filePathCallback
when it gets URI for the chosen file (upon onActivityResult
).
How do you people solve this problem?
I have created a simple project which represents the issue in isolation, feel free to check it out: https://github.com/allco/WebViewFileChooseIssue
Here is some video:
http://www.youtube.com/watch?v=9mnd0lT9lZI
Case #2 demonstrates the problem, the app still shows "No file chosen" even when the choice is done.
Here is the clipped snippet with a key file from the project:
class MainActivity : AppCompatActivity() {
companion object {
const val REQ_CODE_CHOOSER = 1
}
val unencodedHtml = "<input type=file>"
var webViewFileChooseCallback: ValueCallback<Array<Uri>>? = null
private val webClient = object : WebViewClient() {
...
}
private val chromeClient = object : WebChromeClient() {
...
override fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: FileChooserParams?
): Boolean {
if (filePathCallback == null) return false
webViewFileChooseCallback = filePathCallback
startFileChooserActivity("*/*")
return true
}
}
fun startFileChooserActivity(mimeType: String) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = mimeType
intent.addCategory(Intent.CATEGORY_OPENABLE)
// special intent for Samsung file manager
val sIntent = Intent("com.sec.android.app.myfiles.PICK_DATA")
// if you want any file type, you can skip next line
sIntent.putExtra("CONTENT_TYPE", mimeType)
sIntent.addCategory(Intent.CATEGORY_DEFAULT)
val chooserIntent: Intent
if (packageManager.resolveActivity(sIntent, 0) != null) {
// it is device with Samsung file manager
chooserIntent = Intent.createChooser(sIntent, "Open file")
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(intent))
} else {
chooserIntent = Intent.createChooser(intent, "Open file")
}
try {
startActivityForResult(chooserIntent, REQ_CODE_CHOOSER)
} catch (ex: android.content.ActivityNotFoundException) {
Toast.makeText(applicationContext, "No suitable File Manager was found.", Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val webView = findViewById<WebView>(R.id.webView)
webView.webChromeClient = chromeClient
webView.webViewClient = webClient
if (savedInstanceState == null) {
val encodedHtml = Base64.encodeToString(unencodedHtml.toByteArray(), Base64.NO_PADDING)
webView.loadData(encodedHtml, "text/html", "base64")
} else {
webView.restoreState(savedInstanceState)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
findViewById<WebView>(R.id.webView).saveState(outState)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when {
requestCode == REQ_CODE_CHOOSER && resultCode == Activity.RESULT_OK && data != null -> {
val uri = when {
data.dataString != null -> arrayOf(Uri.parse(data.dataString))
data.clipData != null -> (0 until data.clipData!!.itemCount)
.mapNotNull { data.clipData?.getItemAt(it)?.uri }
.toTypedArray()
else -> null
}
webViewFileChooseCallback?.onReceiveValue(uri)
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
}