Skip to content
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

Granting device camera access to my website #336

Open
Hassanmir92 opened this issue Jun 27, 2024 · 7 comments
Open

Granting device camera access to my website #336

Hassanmir92 opened this issue Jun 27, 2024 · 7 comments

Comments

@Hassanmir92
Copy link

Hassanmir92 commented Jun 27, 2024

Hi 👋

I'm having some issues trying to access Camera capabilities in my Android App. I am building an app where one of the functionalities is to use the camera, display it on the page and scan QR codes using html5-qrcode library. This functionality works fine on the Chrome. However, not as an Android app on an Android device.

I believe I am successfully requesting and granting access to my application for the usage of CAMERA, however, I think my external website which is loaded into the TurboWebView also somehow needs access which it is not getting or even requesting.

To help build a picture, here is some of my code:

Here are the permissions and features specified inside AndroidManifest

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.ACTION_IMAGE_CAPTURE" />

    <uses-feature android:name="android.hardware.camera"/>
    <uses-feature android:name="android.hardware.camera2"/>
    <uses-feature android:name="android.hardware.camera.any"/>
    <uses-feature android:name="android.hardware.camera.autofocus"/>

MainActivity

class MainActivity : AppCompatActivity(), TurboActivity {
    override lateinit var delegate: TurboActivityDelegate
    private val CAMERA_PERMISSION_REQUEST_CODE = 1

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.main_activity)

        delegate = TurboActivityDelegate(this, R.id.main_nav_host)

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            this.requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE)
        }
    }

    // For Debugging purposes, this is always returning "Permission granted" in the logs
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
            println("DEBUG -> MainActivity Camera Request: Permission granted")
        } else {
            println("DEBUG -> MainActivity Camera Request: Permission denied")
        }
        return
    }
}

As you can see from my override above, I have implemented onRequestPermissionsResult where I am consistently getting "Camera Request: Permission granted" which means my application definitely has access.

My onSessionCreated() within my MainSessionNavHostFragment (from following demo examples)

    override fun onSessionCreated() {
        super.onSessionCreated()

        val activity = session.webView.context as MainActivity

        // Configure WebView
        session.webView.settings.mediaPlaybackRequiresUserGesture = false;
        session.webView.settings.javaScriptEnabled = true;
        session.webView.settings.domStorageEnabled = true;
        session.webView.settings.allowFileAccess = true;
        session.webView.webChromeClient = NewWebChromeClient(session);
        session.webView.addJavascriptInterface(WebAppInterface(activity), "AndroidApp");
    }

As you can see from the above example, I tried to set my own NewWebChromeClient and additionally a javascript interface which you can see below:

The webapp interface also allows me to simply call AndroidApp.requestCameraPermission() directly from my stimulus JS controller which works, but again it doesn't actually give permission for my actual website to access the camera.

class WebAppInterface(private val mContext: Context) {
    private val CAMERA_PERMISSION_REQUEST_CODE = 1

    @JavascriptInterface
    fun requestCameraPermission() {
        println("DEBUG -> checking permission in Interface")

        val activity = mContext as MainActivity;
        activity.requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE)
    }
}

Here is also my custom WebChromeClient which based on putting some logging in there, never actually runs.

class NewWebChromeClient(session: TurboSession) : TurboWebChromeClient(session) {
    override fun onPermissionRequest(request: PermissionRequest) {
        println("DEBUG -> checking permission is being granted in ChromeClient");
        request.grant(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE));
    }
}

Through testing in developer mode using a real device, I can confirm I definitely get the prompt for giving camera permission. Through my debug logs, everything indicates that permission to the app has been granted. However, these lines of logs sadly disagree 😢 :

CameraManagerGlobal  - Connecting to camera service
CameraManagerGlobal  - Camera 0 facing CAMERA_FACING_BACK state now CAMERA_STATE_CLOSED for client com.sec.android.app.camera API Level 2
CameraManagerGlobal  - Camera 1 facing CAMERA_FACING_FRONT state now CAMERA_STATE_CLOSED for client vendor.client.pid<1235> API Level 2
...
chromium - Uncaught (in promise) Error getting userMedia, error = NotAllowedError: Permission denied

The last line of log comes from where my library is triggering the JS getUserMedia() function.

I may be just missing something obvious here or I'm being delusional in thinking this is supposed to be straight forward 😂. I really appreciate any guidance or help 🙏

@leonvogt
Copy link

Hm, your Android code looks good to me. Don't see anything wrong here.
Two gotchas I run into when working with getUserMedia are:

  • Camera access is only allowed on https urls. If you are using a http url, you will get a NotAllowedError: Permission denied error.
  • You need to have a onloadedmetadata callback like this:
const constraints = {
  audio: false,
  video: true,
};
navigator.mediaDevices
  .getUserMedia(constraints)
  .then((mediaStream) => {
    const video = document.querySelector("video");
    video.srcObject = mediaStream;
    video.onloadedmetadata = () => {
      video.play();
    };
  })

Without the onloadedmetadata callback, you will see the camera access notification light flip to green to show the camera is enabled but won't be able to see the feed at all.

@Hassanmir92
Copy link
Author

@leonvogt Thanks for your response. With regards to both your points, I definitely am using https and I tried adding the onloadedmetadata but it still doesn't work since I get the same NotAllowedError: Permission denied with regards to . getUserMedia() .

@leonvogt
Copy link

Hm ok. I see a difference on how you handle the browser permission request. Don't think that's the issue, but I post my code for granting the camera access here anyway:

override fun onPermissionRequest(request: PermissionRequest?) {
    // if permission request is for camera access
    if (request?.resources?.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE) == true) {
        // always grant permission
        // depending on the android camera permission, the browser will still get a permission denied error if the user has not granted the permission
        request.grant(request.resources)
    }
}

Without this code, the browser will also get a NotAllowedError: Permission denied error when trying to access the camera.

@leonvogt
Copy link

leonvogt commented Jun 28, 2024

Ah wait, I didn't read your issue text carefully enough.
You mentioned:

Here is also my custom WebChromeClient which based on putting some logging in there, never actually runs.

If the ChromeClient doesn't get invoked, it can't handle the camera permission request, which would explain the NotAllowedError: Permission denied error.

I add my ChromeClient a bit differently:

@TurboNavGraphDestination(uri = "turbo://fragment/web")
open class WebFragment : TurboWebFragment(), NavDestination {

    override fun createWebChromeClient(): TurboWebChromeClient {
        return CustomChromeClient(session)
    }
}

@Hassanmir92
Copy link
Author

Hassanmir92 commented Jun 28, 2024

@leonvogt You genius! That did it. I clearly missed that TurboWebFragment was doing createWebChromeClient and that I would need to just override the function.

Really really appreciate your help. I'd been lost for 48 hours over this haha!

@leonvogt
Copy link

Awesome. I'm happy to hear that it worked! 🎉

@harsini66
Copy link

Can you explain this method in Java language as well?
To solve my problem

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

3 participants