1- package neth.iecal.questphone.app.screens.quest.view.external_integration
1+ package neth.iecal.questphone.app.screens.quest.view.external_integration.webview
22
33import android.annotation.SuppressLint
4- import android.content.Context
4+ import android.content.pm.PackageManager
55import android.graphics.Bitmap
66import android.util.Log
77import android.view.ViewGroup
8- import android.webkit.JavascriptInterface
8+ import android.webkit.ConsoleMessage
9+ import android.webkit.PermissionRequest
10+ import android.webkit.WebChromeClient
911import android.webkit.WebResourceError
1012import android.webkit.WebResourceRequest
1113import android.webkit.WebView
1214import android.webkit.WebViewClient
15+ import androidx.activity.compose.rememberLauncherForActivityResult
16+ import androidx.activity.result.contract.ActivityResultContracts
1317import androidx.compose.foundation.background
1418import androidx.compose.foundation.layout.Box
1519import androidx.compose.foundation.layout.fillMaxSize
1620import androidx.compose.foundation.layout.padding
21+ import androidx.compose.material3.AlertDialog
22+ import androidx.compose.material3.Button
1723import androidx.compose.material3.CircularProgressIndicator
1824import androidx.compose.material3.MaterialTheme
1925import androidx.compose.material3.Text
2026import androidx.compose.runtime.Composable
2127import androidx.compose.runtime.getValue
28+ import androidx.compose.runtime.mutableStateMapOf
2229import androidx.compose.runtime.mutableStateOf
2330import androidx.compose.runtime.remember
2431import androidx.compose.runtime.setValue
@@ -30,18 +37,14 @@ import androidx.compose.ui.platform.LocalContext
3037import androidx.compose.ui.text.style.TextAlign
3138import androidx.compose.ui.unit.dp
3239import androidx.compose.ui.viewinterop.AndroidView
33- import kotlinx.io.IOException
34- import neth.iecal.questphone.app.screens.quest.view.ExternalIntegrationQuestViewVM
35- import neth.iecal.questphone.core.utils.reminder.simpleAlarm.AlarmHelper
40+ import androidx.core.content.ContextCompat
41+ import neth.iecal.questphone.app.screens.quest.view.external_integration.ExternalIntegrationQuestViewVM
3642import neth.iecal.questphone.data.CommonQuestInfo
37- import okhttp3.Headers
38- import okhttp3.OkHttpClient
39- import okhttp3.Request
4043import org.json.JSONObject
4144
4245@SuppressLint(" SetJavaScriptEnabled" )
4346@Composable
44- fun WebView (
47+ fun ExtIntWebview (
4548 commonQuestInfo : CommonQuestInfo ,
4649 viewQuestVM : ExternalIntegrationQuestViewVM
4750) {
@@ -54,6 +57,24 @@ fun WebView(
5457 var isError by remember { mutableStateOf(false ) }
5558 var errorMessage by remember { mutableStateOf(" " ) }
5659
60+ val allowedPages = remember { mutableStateMapOf<String , Boolean >() } // URL -> allowed
61+ var pendingPermissionRequest by remember { mutableStateOf<PermissionRequest ?>(null ) }
62+ var pendingRequestUrl by remember { mutableStateOf<String ?>(null ) }
63+
64+ val cameraPermissionLauncher = rememberLauncherForActivityResult(
65+ contract = ActivityResultContracts .RequestPermission ()
66+ ) { granted ->
67+ pendingPermissionRequest?.let { request ->
68+ if (granted) {
69+ // request.grant(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE))
70+ } else {
71+ pendingPermissionRequest?.deny()
72+ pendingPermissionRequest = null
73+ pendingRequestUrl = null
74+ }
75+ }
76+ }
77+
5778 fun createWebView (): WebView {
5879 return WebView (context).apply {
5980 settings.apply {
@@ -77,6 +98,7 @@ fun WebView(
7798 isError = false
7899 }
79100
101+
80102 override fun onPageFinished (view : WebView ? , url : String? ) {
81103 super .onPageFinished(view, url)
82104 isLoading = false
@@ -127,6 +149,36 @@ fun WebView(
127149 }
128150 }
129151
152+ webChromeClient = object : WebChromeClient () {
153+ override fun onPermissionRequest (request : PermissionRequest ) {
154+ val url = webView?.url ? : " Unknown site"
155+
156+ // If page is already allowed → grant immediately
157+ if (allowedPages[url] == true ) {
158+ request.grant(request.resources)
159+ return
160+ }
161+
162+ pendingPermissionRequest = request
163+ pendingRequestUrl = url
164+
165+ val hasCamera = ContextCompat .checkSelfPermission(context, android.Manifest .permission.CAMERA ) ==
166+ PackageManager .PERMISSION_GRANTED
167+
168+ if (! hasCamera) {
169+ // Ask for system permission first
170+ cameraPermissionLauncher.launch(android.Manifest .permission.CAMERA )
171+ }
172+ // Else: system permission already granted → show per-page consent dialog automatically
173+ }
174+ override fun onConsoleMessage (consoleMessage : ConsoleMessage ? ): Boolean {
175+ consoleMessage?.let {
176+ Log .d(" WebViewConsole" , " ${it.message()} -- ${it.sourceId()} :${it.lineNumber()} " )
177+ }
178+ return true
179+ }
180+ }
181+
130182 val json = JSONObject (commonQuestInfo.quest_json)
131183 if (json.has(" webviewUrl" )) {
132184 val url = json.getString(" webviewUrl" )
@@ -136,6 +188,37 @@ fun WebView(
136188 }
137189 }
138190
191+ if (pendingPermissionRequest != null && pendingRequestUrl != null ) {
192+ AlertDialog (
193+ onDismissRequest = {
194+ pendingPermissionRequest?.deny()
195+ pendingPermissionRequest = null
196+ pendingRequestUrl = null
197+ },
198+ title = { Text (" Camera Access Request" ) },
199+ text = { Text (" Do you want to allow ${pendingRequestUrl} to access your camera?" ) },
200+ confirmButton = {
201+ Button (onClick = {
202+ pendingPermissionRequest?.grant(arrayOf(PermissionRequest .RESOURCE_VIDEO_CAPTURE ))
203+ pendingRequestUrl?.let { url -> allowedPages[url] = true }
204+ pendingPermissionRequest = null
205+ pendingRequestUrl = null
206+ }) {
207+ Text (" Allow" )
208+ }
209+ },
210+ dismissButton = {
211+ Button (onClick = {
212+ pendingPermissionRequest?.deny()
213+ pendingPermissionRequest = null
214+ pendingRequestUrl = null
215+ }) {
216+ Text (" Deny" )
217+ }
218+ }
219+ )
220+ }
221+
139222 Box (modifier = Modifier .fillMaxSize().keepScreenOn() ) {
140223 AndroidView (
141224 modifier = Modifier .fillMaxSize(),
@@ -170,85 +253,6 @@ fun WebView(
170253 }
171254 }
172255}
173-
174- class WebAppInterface (private val context : Context , private val webView : WebView , private val viewQuestVM : ExternalIntegrationQuestViewVM ) {
175-
176- private val client = OkHttpClient ()
177-
178- @JavascriptInterface
179- fun onQuestCompleted () {
180- viewQuestVM.saveMarkedQuestToDb()
181- Log .d(" WebAppInterface" , " Quest Completed" )
182- android.widget.Toast .makeText(
183- context,
184- " Quest completed!" ,
185- android.widget.Toast .LENGTH_SHORT
186- ).show()
187- }
188-
189- @JavascriptInterface
190- fun toast (msg : String ) {
191- Log .d(" WebAppInterfaceToast" ,msg)
192- android.widget.Toast .makeText(context, msg, android.widget.Toast .LENGTH_SHORT ).show()
193- }
194- @JavascriptInterface
195- fun isQuestCompleted ():Boolean {
196- return viewQuestVM.isQuestComplete.value
197- }
198- @JavascriptInterface
199- fun enableFullScreen () {
200- viewQuestVM.isFullScreen.value = true
201- }
202- fun disableFullScreen (){
203- viewQuestVM.isFullScreen.value = false
204- }
205- @JavascriptInterface
206- fun setAlarmedNotification (triggerMillis : Long ,title : String ,description : String ){
207- val alarmManager = AlarmHelper (context)
208- alarmManager.setAlarm(triggerMillis,title,description)
209- }
210-
211- @JavascriptInterface
212- fun getCoinRewardRatio ():Int {
213- val sp = context.getSharedPreferences(" minutes_per_5" , Context .MODE_PRIVATE )
214- return sp.getInt(" minutes_per_5" ,10 )
215- }
216- @JavascriptInterface
217- fun fetchDataWithoutCorsAsync (url : String , headersJson : String? , callback : String ) {
218- Log .d(" Webview" ," Fetching without cors" )
219- Thread {
220- val result = try {
221- val builder = Request .Builder ().url(url)
222-
223- // Parse headers JSON from JS
224- headersJson?.let {
225- val json = JSONObject (it)
226- val headersBuilder = Headers .Builder ()
227- json.keys().forEach { key ->
228- headersBuilder.add(key, json.getString(key))
229- }
230- builder.headers(headersBuilder.build())
231- }
232-
233- val request = builder.build()
234- client.newCall(request).execute().use { response ->
235- if (! response.isSuccessful) {
236- " {\" error\" :\" ${response.code} \" }"
237- } else {
238- response.body?.string() ? : " {}"
239- }
240- }
241- } catch (e: IOException ) {
242- " {\" error\" :\" ${e.message} \" }"
243- }
244-
245- // Post result back to JS callback on UI thread
246- webView.post {
247- webView.evaluateJavascript(" $callback (${JSONObject .quote(result)} );" , null )
248- }
249- }.start()
250- }
251- }
252256fun Color.toColorHex (): String {
253257 val r = (red * 255 ).toInt()
254258 val g = (green * 255 ).toInt()
0 commit comments