feat(kiosk): add camera, microphone, storage permissions

- Manifest: CAMERA, RECORD_AUDIO, READ_EXTERNAL_STORAGE, READ_MEDIA_IMAGES
- Runtime: requests all permissions on startup (requestAllPermissions)
- WebView: onPermissionRequest checks runtime grants, requests if needed
- onRequestPermissionsResult grants pending WebView permission after user allows
- Camera and mic now work inside the kiosk WebView
This commit is contained in:
dadaloop82
2026-04-16 19:07:30 +00:00
parent bc70f330f8
commit 9083e25f37
2 changed files with 83 additions and 1 deletions
@@ -9,6 +9,17 @@
<!-- Keep screen on -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Camera & Microphone (for barcode scan, photo, voice) -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<!-- Storage (file upload/download) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!-- Move task to front (bring kiosk back after gateway launch) -->
<uses-permission android:name="android.permission.REORDER_TASKS" />
@@ -1,6 +1,7 @@
package it.dadaloop.evershelf.kiosk
import android.annotation.SuppressLint
import android.Manifest
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
@@ -33,6 +34,8 @@ import android.widget.ScrollView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.material.button.MaterialButton
import org.json.JSONObject
import java.net.URL
@@ -69,8 +72,12 @@ class KioskActivity : AppCompatActivity() {
// File chooser
private var fileChooserCallback: ValueCallback<Array<Uri>>? = null
// Pending WebView permission request (waiting for runtime grant)
private var pendingWebPermission: PermissionRequest? = null
companion object {
private const val FILE_CHOOSER_REQUEST = 1002
private const val PERMISSION_REQUEST_CODE = 1003
private const val PREFS_NAME = "evershelf_kiosk"
private const val KEY_URL = "evershelf_url"
private const val KEY_SETUP_COMPLETE = "setup_complete"
@@ -89,6 +96,7 @@ class KioskActivity : AppCompatActivity() {
bindViews()
enterImmersiveMode()
enableKioskLock()
requestAllPermissions()
// Show splash then proceed
Handler(Looper.getMainLooper()).postDelayed({
@@ -171,6 +179,46 @@ class KioskActivity : AppCompatActivity() {
}
}
// ── Runtime Permissions ─────────────────────────────────────────────
private fun requestAllPermissions() {
val needed = mutableListOf<String>()
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
needed.add(Manifest.permission.CAMERA)
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
needed.add(Manifest.permission.RECORD_AUDIO)
}
if (Build.VERSION.SDK_INT >= 33) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) {
needed.add(Manifest.permission.READ_MEDIA_IMAGES)
}
} else if (Build.VERSION.SDK_INT <= 32) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
needed.add(Manifest.permission.READ_EXTERNAL_STORAGE)
}
}
if (needed.isNotEmpty()) {
ActivityCompat.requestPermissions(this, needed.toTypedArray(), PERMISSION_REQUEST_CODE)
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSION_REQUEST_CODE) {
// Grant pending WebView permission if camera/mic were just granted
pendingWebPermission?.let { req ->
val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
if (allGranted) {
req.grant(req.resources)
} else {
req.deny()
}
pendingWebPermission = null
}
}
}
// ── Triple-tap to exit ────────────────────────────────────────────────
private fun handleTripleTap() {
@@ -417,7 +465,30 @@ class KioskActivity : AppCompatActivity() {
webView.webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest?) {
runOnUiThread { request?.grant(request.resources) }
request ?: return
runOnUiThread {
val needed = mutableListOf<String>()
for (res in request.resources) {
when (res) {
PermissionRequest.RESOURCE_VIDEO_CAPTURE -> {
if (ContextCompat.checkSelfPermission(this@KioskActivity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
needed.add(Manifest.permission.CAMERA)
}
}
PermissionRequest.RESOURCE_AUDIO_CAPTURE -> {
if (ContextCompat.checkSelfPermission(this@KioskActivity, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
needed.add(Manifest.permission.RECORD_AUDIO)
}
}
}
}
if (needed.isEmpty()) {
request.grant(request.resources)
} else {
pendingWebPermission = request
ActivityCompat.requestPermissions(this@KioskActivity, needed.toTypedArray(), PERMISSION_REQUEST_CODE)
}
}
}
override fun onConsoleMessage(msg: ConsoleMessage?): Boolean = true
override fun onShowFileChooser(