6

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)
        }
    }
}

Alexander Skvortsov
  • 2,696
  • 2
  • 17
  • 31

0 Answers0