1

I am trying to open a file chooser from a Webview and select a file. But in certain phones after the camera is opened, My activity gets destroyed and when the user clicks a picture and comes back, the activity is recreated. I maintain the webview state using the URL, but the callback is lost.

class SampleWebActivity : AppCompatActivity() {
    private var mUploadMessage: ValueCallback<Array<Uri>>? = null


    private var mCM: String? = null
    private val FILE_CHOOSER_REQUEST_CODE = 1111
    private var uploadType: Int = TYPE_BOTH
    

    private lateinit var binding: ActivitySampleWebActivity

    @SuppressLint("SetJavaScriptEnabled")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivitySampleWebActivity.inflate(layoutInflater)
        setContentView(binding.root)
        

        binding.sampleWebView.settings.javaScriptEnabled = true
        binding.sampleWebView.settings.domStorageEnabled = true
        binding.sampleWebView.webChromeClient = sampleChromeClient()
        binding.sampleWebView.webViewClient = sampleWebViewClient()

        loadUrl()
    }

    

    

    override fun onActivityResult(
        requestCode: Int,
        resultCode: Int,
        intent: Intent?
    ) {
        super.onActivityResult(requestCode, resultCode, intent)
        var results: Array<Uri>? = null
        //Check if response is positive
        if (resultCode == Activity.RESULT_OK) {
            if (requestCode == FILE_CHOOSER_REQUEST_CODE) {
                if (null == mUploadMessage) { //this returns true as callback is null
                    return
                }
                if (intent == null) {
                    //Capture Photo if no image available
                    if (mCM != null) {
                        results = arrayOf(Uri.parse(mCM))
                    }
                } else {
                    val dataString = intent.dataString
                    if (dataString != null) {
                        results = arrayOf(Uri.parse(dataString))
                    }else{
                        if (mCM != null) {
                            results = arrayOf(Uri.parse(mCM))
                        }
                    }
                }
            }
        }
        mUploadMessage?.onReceiveValue(results)
        mUploadMessage = null

    }





    private inner class sampleChromeClient : WebChromeClient() {

        @SuppressLint("LogNotTimber")
        override fun onShowFileChooser(
            mWebView: WebView,
            filePathCallback: ValueCallback<Array<Uri>>,
            fileChooserParams: FileChooserParams
        ): Boolean {

            if (mUploadMessage != null){
                mUploadMessage?.onReceiveValue(null)
            }
            cameraPermissionIntentData = CameraPermissionIntentData(null, arrayOf(), null, null)
            mUploadMessage = filePathCallback
            var takePictureIntent: Intent? = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
            if (takePictureIntent!!.resolveActivity(this@sampleWebActivity.packageManager) != null) {
                var photoFile: File? = null
                try {
                    photoFile = createImageFile()
                } catch (ex: IOException) {
                    Log.e(TAG, "Image file creation failed", ex)
                }

                if (photoFile != null) {
                    mCM = "file:" + photoFile.absolutePath
                    takePictureIntent.putExtra("PhotoPath", mCM)
                    val uri = FileProvider.getUriForFile(this@sampleWebActivity, this@sampleWebActivity.applicationContext.packageName + ".provider", photoFile)
                    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
                } else {
                    takePictureIntent = null
                }
            }

            val chooserIntent = Intent(Intent.ACTION_CHOOSER)
            val intentArray: Array<Intent?>
            val contentSelectionIntent = Intent(Intent.ACTION_GET_CONTENT)
            contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE)
            contentSelectionIntent.type = "*/*"
            chooserIntent.putExtra(Intent.EXTRA_TITLE, "Select file to send")

            when(uploadType){

                TYPE_CAMERA -> {
                    if(takePictureIntent != null) {
                        cameraPermissionIntentData.takePictureIntent = takePictureIntent
                        checkCameraPermission(
                                SnackBarRationale(
                                        context = this@sampleWebActivity,
                                        view = binding.sampleWebView,
                                        rationaleText = getString(R.string.camera_access_required),
                                        requestCode = TYPE_CAMERA
                                )
                                , openCamera = {
                            cameraPermissionIntentData.takePictureIntent?.let {
                                startActivityForResult(it, FILE_CHOOSER_REQUEST_CODE)
                            } ?: Log.logToCrashlytics(" +++++ take picture intent is null , permission already present")
                        })
                    } else {
                        return false
                    }
                }

                TYPE_GALLERY -> {
                    chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent)
                    startActivityForResult(chooserIntent, FILE_CHOOSER_REQUEST_CODE)
                }

                TYPE_BOTH -> {
                    intentArray = if (takePictureIntent != null) {
                        arrayOf(takePictureIntent)
                    } else {
                        arrayOf()
                    }
                    chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent)
                    chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray)

                    cameraPermissionIntentData.apply {
                        this.takePictureIntent = takePictureIntent
                        this.intentArray = intentArray
                        this.contentSelectionIntent = contentSelectionIntent
                        this.chooserIntent = chooserIntent
                    }

                    checkCameraPermission(
                            SnackBarRationale(
                                    context = this@sampleWebActivity,
                                    view = binding.sampleWebView,
                                    rationaleText = getString(R.string.camera_access_required),
                                    requestCode = TYPE_BOTH
                            )
                            , openCamera = {
                        cameraPermissionIntentData.chooserIntent?.let {
                            startActivityForResult(it, FILE_CHOOSER_REQUEST_CODE)
                        } ?: Log.logToCrashlytics(" +++++  chooser intent is null , permission already present")
                    })
                }

            }

            return true
        }

        override fun onProgressChanged(view: WebView?, newProgress: Int) {
            super.onProgressChanged(view, newProgress)

            if (newProgress == 100) {
                binding.sampleWebPb.postDelayed({
                    binding.sampleWebPb.visibility = View.GONE
                }, 2000)
            }
        }
    }

I tried to save the callback and file path outside the activity (in a singleton) and using it again when activity is created, but it didn't work. I even tried to save the SampleChromeClient and use the same instance again but still the selected file doesn't load. What can I do?

dhiraj uchil
  • 113
  • 10
  • Try to use ViewModel to save data from the result your activity or better to read about new approaches https://medium.com/realm/startactivityforresult-is-deprecated-82888d149f5d – Yurii Dec 12 '22 at 12:37
  • 1
    I can save the data from the activity result, but the issue is that I can't pass it to the webview because the callback is lost. – dhiraj uchil Dec 12 '22 at 13:43
  • Yes, becasue callback was destroed with activity, so better to persist data into viewmodel and pass in into loadUrl() as parameter. – Yurii Dec 12 '22 at 13:48
  • ViewModel won't save you if Android kills the whole process. I'm not sure if it's possible to save the ValueCallback in case of process death but it would be very helpful. – kecal909 Jan 02 '23 at 14:40
  • 1
    Actually this is probably not a process death. I had the same issue on Samsung S22 - the activity was being re-created twice after returning from the camera. I found out that it was because the camera forced landscape orientation to my activity and it had to be re-created back. I was able to fix this by declaring configChanges in manifest as per this answer https://stackoverflow.com/a/10411504 – kecal909 Jan 03 '23 at 13:18
  • @kecal909 you're right the issue was due to screen rotation. Making changes in manifest fixes the issue. Can you add it as an answer? I will mark it as the accepted solution. – dhiraj uchil Jan 04 '23 at 13:12

1 Answers1

0

The activity is being recreated because of orientation changes. Even if you force your activity to always use portrait orientation this will happen on certain phones.

For me it was happening on Samsung S22. The camera app changes the orientation to landscape (on some phones). And when the user gets back from the camera the app notices that it is in landscape mode and will kill and recreate itself back to portrait. But you won't be able to use the file chooser callback anymore even if you save the screen state.

To fix this issue you need to declare that your activity handles orientation changes manually in AndroidManifest like this:

<activity
    android:name=".YourActivity"
    android:configChanges="orientation|keyboardHidden|screenSize"
    android:screenOrientation="portrait" />

Because you declare that you want to handle orientation (and screen size!) events manually the activity won't be killed and recreated anymore and you won't lose the file chooser callback. When orientation change happens onConfigurationChanged() callback will be invoked in your activity and you should handle it manually (if you need to).

Link to documentation for configChanges tag in manifest: https://developer.android.com/guide/topics/manifest/activity-element.html#config

kecal909
  • 156
  • 2
  • 10