package {{.PackageName}} import android.Manifest import android.content.ContentResolver import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.database.Cursor import android.net.Uri import android.os.Build import android.os.VibrationEffect import android.os.Vibrator import android.provider.ContactsContract import android.provider.MediaStore import android.webkit.JavascriptInterface import android.webkit.WebView import android.widget.Toast import androidx.core.content.ContextCompat import org.json.JSONArray import org.json.JSONObject /** * JavaScript interface for exposing Android native features to WebView * Accessible via window.Android in JavaScript */ class PermissionBridge(private val activity: MainActivity, private val webView: WebView) { /** * Check if a specific permission is granted * @param permission Permission name (e.g., "camera", "location", "contacts") * @return true if permission is granted, false otherwise */ @JavascriptInterface fun hasPermission(permission: String): Boolean { val androidPermission = when (permission.lowercase()) { "camera" -> Manifest.permission.CAMERA "location" -> Manifest.permission.ACCESS_FINE_LOCATION "contacts" -> Manifest.permission.READ_CONTACTS "microphone", "audio" -> Manifest.permission.RECORD_AUDIO "storage" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { Manifest.permission.READ_MEDIA_IMAGES } else { Manifest.permission.READ_EXTERNAL_STORAGE } else -> return false } return ContextCompat.checkSelfPermission( activity, androidPermission ) == PackageManager.PERMISSION_GRANTED } /** * Get app information * @return JSON string with app name, package, version */ @JavascriptInterface fun getAppInfo(): String { return try { val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0) JSONObject().apply { put("appName", "{{.AppName | escapeKotlin}}") put("packageName", "{{.PackageName}}") put("versionName", packageInfo.versionName) put("versionCode", if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { packageInfo.longVersionCode } else { @Suppress("DEPRECATION") packageInfo.versionCode.toLong() }) }.toString() } catch (e: Exception) { errorResponse("Failed to get app info: ${e.message}") } } /** * Vibrate the device * @param durationMs Vibration duration in milliseconds */ @JavascriptInterface fun vibrate(durationMs: Int) { {{if .Permissions.Vibrate}} try { val vibrator = activity.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { vibrator.vibrate(VibrationEffect.createOneShot(durationMs.toLong(), VibrationEffect.DEFAULT_AMPLITUDE)) } else { @Suppress("DEPRECATION") vibrator.vibrate(durationMs.toLong()) } } catch (e: Exception) { // Silently fail - vibration is not critical } {{else}} // Vibrate permission not enabled {{end}} } /** * Show an Android toast message * @param message Message to display * @param duration Duration: "short" or "long" (default: short) */ @JavascriptInterface fun showToast(message: String, duration: String = "short") { val toastDuration = if (duration.lowercase() == "long") { Toast.LENGTH_LONG } else { Toast.LENGTH_SHORT } activity.runOnUiThread { Toast.makeText(activity, message, toastDuration).show() } } /** * Get all contacts * @return JSON array of contacts with id, name, phone */ @JavascriptInterface fun getAllContacts(): String { {{if .Permissions.ReadContacts}} if (!hasPermission("contacts")) { return errorResponse("READ_CONTACTS permission not granted") } return try { val contacts = JSONArray() val contentResolver: ContentResolver = activity.contentResolver val cursor: Cursor? = contentResolver.query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, arrayOf( ContactsContract.CommonDataKinds.Phone.CONTACT_ID, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, ContactsContract.CommonDataKinds.Phone.NUMBER ), null, null, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC" ) cursor?.use { val idIndex = it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_ID) val nameIndex = it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME) val numberIndex = it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER) while (it.moveToNext()) { val contact = JSONObject().apply { put("id", it.getString(idIndex)) put("name", it.getString(nameIndex)) put("phone", it.getString(numberIndex)) } contacts.put(contact) } } JSONObject().apply { put("success", true) put("contacts", contacts) }.toString() } catch (e: Exception) { errorResponse("Failed to read contacts: ${e.message}") } {{else}} return errorResponse("Contacts permission not enabled in app settings") {{end}} } /** * Share content using native Android share dialog * @param text Text to share * @param url URL to share (optional) */ @JavascriptInterface fun shareContent(text: String, url: String = "") { activity.runOnUiThread { try { val shareIntent = Intent(Intent.ACTION_SEND).apply { type = "text/plain" putExtra(Intent.EXTRA_TEXT, if (url.isNotEmpty()) "$text\n$url" else text) } activity.startActivity(Intent.createChooser(shareIntent, "Share via")) } catch (e: Exception) { Toast.makeText(activity, "Failed to share: ${e.message}", Toast.LENGTH_SHORT).show() } } } /** * Open URL in external browser * @param url URL to open */ @JavascriptInterface fun openExternalBrowser(url: String) { activity.runOnUiThread { try { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) activity.startActivity(intent) } catch (e: Exception) { Toast.makeText(activity, "Failed to open URL: ${e.message}", Toast.LENGTH_SHORT).show() } } } /** * Get device information * @return JSON string with device model, manufacturer, Android version */ @JavascriptInterface fun getDeviceInfo(): String { return try { JSONObject().apply { put("manufacturer", Build.MANUFACTURER) put("model", Build.MODEL) put("androidVersion", Build.VERSION.RELEASE) put("sdkVersion", Build.VERSION.SDK_INT) put("device", Build.DEVICE) put("brand", Build.BRAND) }.toString() } catch (e: Exception) { errorResponse("Failed to get device info: ${e.message}") } } /** * Check if the device has a specific feature * @param feature Feature name (e.g., "camera", "bluetooth", "nfc") * @return true if feature is available, false otherwise */ @JavascriptInterface fun hasFeature(feature: String): Boolean { val featureString = when (feature.lowercase()) { "camera" -> "android.hardware.camera" "frontcamera" -> "android.hardware.camera.front" "bluetooth" -> "android.hardware.bluetooth" "nfc" -> "android.hardware.nfc" "gps" -> "android.hardware.location.gps" "wifi" -> "android.hardware.wifi" "telephony" -> "android.hardware.telephony" else -> return false } return activity.packageManager.hasSystemFeature(featureString) } /** * Log a message (useful for debugging) * @param tag Log tag * @param message Log message */ @JavascriptInterface fun log(tag: String, message: String) { println("WebView[$tag]: $message") } // ===== New Bridge Methods ===== /** * Request a permission on-demand * @param permission Permission name (camera, location, contacts, microphone, storage) * @param callbackId JavaScript callback ID */ @JavascriptInterface fun requestPermission(permission: String, callbackId: String) { val (androidPermission, rationale) = when (permission.lowercase()) { "camera" -> Pair(Manifest.permission.CAMERA, "Camera access is needed for taking photos") "location" -> Pair(Manifest.permission.ACCESS_FINE_LOCATION, "Location access is needed for location-based features") "contacts" -> Pair(Manifest.permission.READ_CONTACTS, "Contacts access is needed for contact-related features") "microphone", "audio" -> Pair(Manifest.permission.RECORD_AUDIO, "Microphone access is needed for audio recording") "storage" -> Pair( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { Manifest.permission.READ_MEDIA_IMAGES } else { Manifest.permission.READ_EXTERNAL_STORAGE }, "Storage access is needed for saving and loading files" ) "notifications" -> Pair( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { Manifest.permission.POST_NOTIFICATIONS } else { "" }, "Notification permission is needed for push notifications" ) else -> { executeCallback(callbackId, "false") return } } if (androidPermission.isEmpty()) { // Permission not needed on this Android version executeCallback(callbackId, "true") return } activity.requestPermissionWithRationale( androidPermission, permission, rationale ) { granted -> executeCallback(callbackId, if (granted) "true" else "false") } } {{if .Permissions.Location}} /** * Get current device location * @param callbackId JavaScript callback ID */ @JavascriptInterface fun getLocation(callbackId: String) { if (!hasPermission("location")) { executeCallback(callbackId, errorResponse("Location permission not granted")) return } activity.getCurrentLocation { location -> if (location != null) { val result = JSONObject().apply { put("success", true) put("latitude", location.latitude) put("longitude", location.longitude) put("accuracy", location.accuracy) put("altitude", location.altitude) put("speed", location.speed) put("bearing", location.bearing) put("timestamp", location.time) } executeCallback(callbackId, result.toString()) } else { executeCallback(callbackId, errorResponse("Failed to get location")) } } } {{end}} {{if .Permissions.Camera}} /** * Take a photo using the camera * @param callbackId JavaScript callback ID */ @JavascriptInterface fun takePhoto(callbackId: String) { if (!hasPermission("camera")) { executeCallback(callbackId, errorResponse("Camera permission not granted")) return } activity.takePhoto { base64 -> if (base64 != null) { val result = JSONObject().apply { put("success", true) put("data", base64) put("mimeType", "image/jpeg") } executeCallback(callbackId, result.toString()) } else { executeCallback(callbackId, errorResponse("Failed to capture photo")) } } } {{end}} {{if .Permissions.Storage}} /** * Pick an image from gallery * @param callbackId JavaScript callback ID */ @JavascriptInterface fun pickImage(callbackId: String) { activity.pickImage { base64 -> if (base64 != null) { val result = JSONObject().apply { put("success", true) put("data", base64) put("mimeType", "image/jpeg") } executeCallback(callbackId, result.toString()) } else { executeCallback(callbackId, errorResponse("Failed to pick image")) } } } {{end}} /** * Copy text to clipboard * @param text Text to copy * @return true if successful */ @JavascriptInterface fun copyToClipboard(text: String): Boolean { return activity.copyToClipboard(text) } /** * Save data to persistent storage * @param key Storage key * @param value Value to store */ @JavascriptInterface fun saveData(key: String, value: String) { activity.saveData(key, value) } /** * Get data from persistent storage * @param key Storage key * @return Stored value or null */ @JavascriptInterface fun getData(key: String): String { return activity.getData(key) ?: "" } /** * Remove data from persistent storage * @param key Storage key */ @JavascriptInterface fun removeData(key: String) { activity.removeData(key) } {{if .FirebaseEnabled}} /** * Get FCM push notification token * @param callbackId JavaScript callback ID */ @JavascriptInterface fun getPushToken(callbackId: String) { activity.getPushToken { token -> if (token != null) { val result = JSONObject().apply { put("success", true) put("token", token) } executeCallback(callbackId, result.toString()) } else { executeCallback(callbackId, errorResponse("Failed to get push token")) } } } {{end}} /** * Execute a JavaScript callback with result * @param callbackId Callback ID * @param result Result to pass to callback */ private fun executeCallback(callbackId: String, result: String) { activity.runOnUiThread { val script = "window.__appBridgeCallbacks && window.__appBridgeCallbacks['$callbackId'] && window.__appBridgeCallbacks['$callbackId']($result);" webView.evaluateJavascript(script, null) } } // Helper function to create error JSON response private fun errorResponse(message: String): String { return JSONObject().apply { put("success", false) put("error", message) }.toString() } }