Skip to content

Commit 0e965be

Browse files
committed
changes:
1 parent 48cf651 commit 0e965be

File tree

6 files changed

+353
-94
lines changed

6 files changed

+353
-94
lines changed

app/src/main/java/neth/iecal/questphone/app/screens/quest/view/external_integration/ExternalIntegrationQuestView.kt

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package neth.iecal.questphone.app.screens.quest.view
1+
package neth.iecal.questphone.app.screens.quest.view.external_integration
22

33
import android.app.Application
44
import androidx.compose.foundation.Image
@@ -30,11 +30,15 @@ import androidx.compose.ui.unit.dp
3030
import androidx.compose.ui.zIndex
3131
import androidx.hilt.navigation.compose.hiltViewModel
3232
import dagger.hilt.android.lifecycle.HiltViewModel
33+
import kotlinx.coroutines.Dispatchers
3334
import kotlinx.coroutines.flow.MutableStateFlow
35+
import kotlinx.coroutines.flow.first
36+
import kotlinx.coroutines.withContext
3437
import neth.iecal.questphone.app.screens.components.TopBarActions
3538
import neth.iecal.questphone.app.screens.game.quickRewardUser
39+
import neth.iecal.questphone.app.screens.quest.view.ViewQuestVM
3640
import neth.iecal.questphone.app.screens.quest.view.dialogs.QuestSkipperDialog
37-
import neth.iecal.questphone.app.screens.quest.view.external_integration.WebView
41+
import neth.iecal.questphone.app.screens.quest.view.external_integration.webview.ExtIntWebview
3842
import neth.iecal.questphone.app.theme.LocalCustomTheme
3943
import neth.iecal.questphone.app.theme.smoothYellow
4044
import neth.iecal.questphone.backed.repositories.QuestRepository
@@ -43,7 +47,9 @@ import neth.iecal.questphone.backed.repositories.UserRepository
4347
import neth.iecal.questphone.data.CommonQuestInfo
4448
import nethical.questphone.core.core.utils.VibrationHelper
4549
import nethical.questphone.core.core.utils.formatHour
50+
import nethical.questphone.data.R
4651
import nethical.questphone.data.game.InventoryItem
52+
import nethical.questphone.data.json
4753
import nethical.questphone.data.xpToRewardForQuest
4854
import javax.inject.Inject
4955

@@ -56,6 +62,15 @@ class ExternalIntegrationQuestViewVM @Inject constructor (questRepository: Quest
5662
fun addQuickReward(coins:Int){
5763
quickRewardUser(coins)
5864
}
65+
fun getUserData():String{
66+
return json.encodeToString(userRepository.userInfo)
67+
}
68+
suspend fun getQuestStats(onDone:(String)->Unit){
69+
val stats = statsRepository.getStatsByQuestId(commonQuestInfo.id).first()
70+
withContext(Dispatchers.Main) {
71+
onDone( json.encodeToString(stats))
72+
}
73+
}
5974
}
6075

6176
@OptIn(ExperimentalMaterial3Api::class)
@@ -90,7 +105,7 @@ fun ExternalIntegrationQuestView(
90105
) {
91106
if (!isQuestComplete && viewModel.getInventoryItemCount(InventoryItem.QUEST_SKIPPER) > 0) {
92107
Image(
93-
painter = painterResource(nethical.questphone.data.R.drawable.quest_skipper),
108+
painter = painterResource(R.drawable.quest_skipper),
94109
contentDescription = "use quest skipper",
95110
modifier = Modifier.size(30.dp)
96111
.clickable {
@@ -104,7 +119,7 @@ fun ExternalIntegrationQuestView(
104119
}) { innerPadding ->
105120

106121
Box(Modifier.fillMaxSize().zIndex(-1f)) {
107-
WebView(commonQuestInfo, viewModel)
122+
ExtIntWebview(commonQuestInfo, viewModel)
108123
}
109124
QuestSkipperDialog(viewModel)
110125
if (!isFullScreen) {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package neth.iecal.questphone.app.screens.quest.view.external_integration.webview
2+
3+
import android.content.BroadcastReceiver
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.content.IntentFilter
7+
import android.webkit.WebView
8+
import nethical.questphone.data.json
9+
10+
object BroadcastCenter {
11+
private var receiver: BroadcastReceiver? = null
12+
13+
fun register(context: Context, webView: WebView, actions: List<String>) {
14+
unregister(context)
15+
16+
val filter = IntentFilter()
17+
actions.forEach { filter.addAction(it) }
18+
19+
receiver = object : BroadcastReceiver() {
20+
override fun onReceive(ctx: Context?, intent: Intent?) {
21+
val action = intent?.action ?: return
22+
val data = intent.getStringExtra("payload") ?: "{}"
23+
24+
// send to JS
25+
val jsCode = "window.onBroadcast && window.onBroadcast(${json.encodeToString(action)}, ${data})"
26+
webView.post { webView.evaluateJavascript(jsCode, null) }
27+
}
28+
}
29+
context.registerReceiver(receiver, filter)
30+
}
31+
32+
fun unregister(context: Context) {
33+
receiver?.let {
34+
context.unregisterReceiver(it)
35+
receiver = null
36+
}
37+
}
38+
}

app/src/main/java/neth/iecal/questphone/app/screens/quest/view/external_integration/Webview.kt renamed to app/src/main/java/neth/iecal/questphone/app/screens/quest/view/external_integration/webview/ExtIntWebview.kt

Lines changed: 93 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
1-
package neth.iecal.questphone.app.screens.quest.view.external_integration
1+
package neth.iecal.questphone.app.screens.quest.view.external_integration.webview
22

33
import android.annotation.SuppressLint
4-
import android.content.Context
4+
import android.content.pm.PackageManager
55
import android.graphics.Bitmap
66
import android.util.Log
77
import android.view.ViewGroup
8-
import android.webkit.JavascriptInterface
8+
import android.webkit.ConsoleMessage
9+
import android.webkit.PermissionRequest
10+
import android.webkit.WebChromeClient
911
import android.webkit.WebResourceError
1012
import android.webkit.WebResourceRequest
1113
import android.webkit.WebView
1214
import android.webkit.WebViewClient
15+
import androidx.activity.compose.rememberLauncherForActivityResult
16+
import androidx.activity.result.contract.ActivityResultContracts
1317
import androidx.compose.foundation.background
1418
import androidx.compose.foundation.layout.Box
1519
import androidx.compose.foundation.layout.fillMaxSize
1620
import androidx.compose.foundation.layout.padding
21+
import androidx.compose.material3.AlertDialog
22+
import androidx.compose.material3.Button
1723
import androidx.compose.material3.CircularProgressIndicator
1824
import androidx.compose.material3.MaterialTheme
1925
import androidx.compose.material3.Text
2026
import androidx.compose.runtime.Composable
2127
import androidx.compose.runtime.getValue
28+
import androidx.compose.runtime.mutableStateMapOf
2229
import androidx.compose.runtime.mutableStateOf
2330
import androidx.compose.runtime.remember
2431
import androidx.compose.runtime.setValue
@@ -30,18 +37,14 @@ import androidx.compose.ui.platform.LocalContext
3037
import androidx.compose.ui.text.style.TextAlign
3138
import androidx.compose.ui.unit.dp
3239
import 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
3642
import neth.iecal.questphone.data.CommonQuestInfo
37-
import okhttp3.Headers
38-
import okhttp3.OkHttpClient
39-
import okhttp3.Request
4043
import 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-
}
252256
fun Color.toColorHex(): String {
253257
val r = (red * 255).toInt()
254258
val g = (green * 255).toInt()

0 commit comments

Comments
 (0)