Skip to content

Instantly share code, notes, and snippets.

@nofxx
Last active June 28, 2025 00:47
Show Gist options
  • Save nofxx/f0974447185466cfa9356fbe374214a3 to your computer and use it in GitHub Desktop.
Save nofxx/f0974447185466cfa9356fbe374214a3 to your computer and use it in GitHub Desktop.
package com.fireho.queroir
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.View
import android.webkit.JavascriptInterface
import android.webkit.WebView
import androidx.activity.enableEdgeToEdge
import dev.hotwire.navigation.activities.HotwireActivity
import dev.hotwire.navigation.navigator.NavigatorConfiguration
import dev.hotwire.navigation.util.applyDefaultImeWindowInsets
class MainActivity : HotwireActivity() {
private lateinit var qrCodeScanner: QrCodeScanner
private var webView: WebView? = null
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<View>(R.id.main_nav_host).applyDefaultImeWindowInsets()
// Initialize QR code scanner
qrCodeScanner = QrCodeScanner(this)
// Delay setup to ensure WebView is loaded
Handler(Looper.getMainLooper()).postDelayed({
findWebViewAndSetupInterface()
}, 1000)
}
/**
* Find WebView and set up JavaScript interface
*/
private fun findWebViewAndSetupInterface() {
try {
// Find the WebView
webView = findWebView()
if (webView != null) {
Log.d("MainActivity", "WebView found, setting up JavaScript interface")
setupJavaScriptInterface(webView!!)
} else {
Log.e("MainActivity", "WebView not found, retrying in 1 second")
// Retry after a delay
Handler(Looper.getMainLooper()).postDelayed({
findWebViewAndSetupInterface()
}, 1000)
}
} catch (e: Exception) {
Log.e("MainActivity", "Error finding WebView", e)
}
}
/**
* Set up JavaScript interface for QR code scanning
*/
private fun setupJavaScriptInterface(webView: WebView) {
try {
Log.d("MainActivity", "Setting up JavaScript interface")
// Add JavaScript interface
webView.addJavascriptInterface(object {
/**
* Perform an action from JavaScript
*/
@JavascriptInterface
fun perform(action: String) {
Log.d("MainActivity", "perform action received: $action")
if (action == "scanQrCode") {
runOnUiThread {
qrCodeScanner.scan()
}
}
}
}, "HotwireNative")
// Inject JavaScript to set up the callbacks system
val jsSetup = """
(function() {
// Set up HotwireNative object if it doesn't exist
window.HotwireNative = window.HotwireNative || {};
// Set a flag to indicate that HotwireNative is available
window.HotwireNative.isAvailable = true;
console.log('Setting up HotwireNative JavaScript interface');
// Initialize callbacks object if it doesn't exist
if (!window.HotwireNative.callbacks) {
window.HotwireNative.callbacks = {};
}
// Add registerCallback function if it doesn't exist
if (typeof window.HotwireNative.registerCallback !== 'function') {
window.HotwireNative.registerCallback = function(name, callback) {
window.HotwireNative.callbacks[name] = callback;
console.log('Registered callback: ' + name);
};
}
console.log('HotwireNative JavaScript interface setup complete');
// Dispatch an event to notify the web app that HotwireNative is available
var event = new CustomEvent('hotwireNativeAvailable');
document.dispatchEvent(event);
})();
""".trimIndent()
webView.evaluateJavascript(jsSetup) { result ->
Log.d("MainActivity", "JavaScript setup result: $result")
}
Log.d("MainActivity", "JavaScript interface setup complete")
} catch (e: Exception) {
Log.e("MainActivity", "Error setting up JavaScript interface", e)
}
}
/**
* Find the WebView in the view hierarchy
*/
private fun findWebView(): WebView? {
try {
// Try to find WebView in the navigator host
val navHost = findViewById<View>(R.id.main_nav_host)
// Log the view hierarchy for debugging
Log.d("MainActivity", "NavHost: $navHost")
// Try different approaches to find the WebView
// Approach 1: Direct child
val webView1 = navHost?.findViewById<WebView>(android.R.id.content)
if (webView1 != null) {
Log.d("MainActivity", "WebView found using approach 1")
return webView1
}
// Approach 2: Search all children recursively
val webView2 = findWebViewInViewHierarchy(navHost)
if (webView2 != null) {
Log.d("MainActivity", "WebView found using approach 2")
return webView2
}
Log.e("MainActivity", "WebView not found in view hierarchy")
return null
} catch (e: Exception) {
Log.e("MainActivity", "Error finding WebView", e)
return null
}
}
/**
* Find WebView in view hierarchy recursively
*/
private fun findWebViewInViewHierarchy(view: View?): WebView? {
if (view == null) return null
if (view is WebView) return view
try {
if (view is android.view.ViewGroup) {
for (i in 0 until view.childCount) {
val child = view.getChildAt(i)
val webView = findWebViewInViewHierarchy(child)
if (webView != null) return webView
}
}
} catch (e: Exception) {
Log.e("MainActivity", "Error searching view hierarchy", e)
}
return null
}
override fun navigatorConfigurations() = listOf(
NavigatorConfiguration(
name = "main",
// startLocation = "https://hotwire-native-demo.dev",
startLocation = "https://queroir.ai",
navigatorHostId = R.id.main_nav_host
)
)
// Handle QR code scan result
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// Get the current WebView
val currentWebView = webView
if (currentWebView != null) {
qrCodeScanner.handleScanResult(requestCode, resultCode, data, currentWebView)
} else {
Log.e("MainActivity", "No WebView available to handle QR code result")
// Try to find WebView again
val foundWebView = findWebView()
if (foundWebView != null) {
qrCodeScanner.handleScanResult(requestCode, resultCode, data, foundWebView)
} else {
Log.e("MainActivity", "Still no WebView available to handle QR code result")
}
}
}
}

QueroIr Mobile Apps Implementation

  • QR Reader
  • NFC Reader
  • Wallet Tag

QR Reader

Fica em looping continuo, leu, manda um POST para queroir.ai/tags/check Resposta uma mensagem, e piscar a tela verde/vermelho Som diferente na leitura tambem bem interessante

NFC Reader

Praticamente a mesma coisa... ^

Usuario muda facilmente modo camera / modo nfc Ou o modo camera sempre ta nfc ativo? Possivel? Mas camera pode ser desligada pra consumir menos bateria

Wallet Tag

Sera gerado um png/webp pra exibir e colocar na carteira a imagem e o codigo NFC

Android Side

  1. QrCodeScanner.kt: Handles the QR code scanning using ZXing library

    • scan(): Opens the camera to scan a QR code
    • handleScanResult(): Processes the scan result
    • sendResultToJavaScript(): Sends the result back to the web view via JavaScript evaluation
    • postResultToRails(): Posts the result to the Rails check route using JavaScript form submission
  2. MainActivity.kt: Integrates the QR code scanner with Hotwire Native

    • Uses delayed initialization to ensure the WebView is fully loaded
    • Implements recursive WebView finding in the view hierarchy
    • Adds a JavaScript interface for the QR code scanner
    • Sets up the JavaScript environment for callbacks
    • Sets a flag to indicate that HotwireNative is available
    • Dispatches a custom event to notify the web app
    • Handles the scan result in onActivityResult

Web Side

  1. qr_code_scanner_controller.js: Stimulus controller for QR code scanning

    • Listens for the 'hotwireNativeAvailable' event
    • Checks if running in native app environment
    • Registers a callback to receive scan results from the native app
    • Handles the scan result and updates the UI
    • Posts the result to the server
    • Includes retry logic and better error messages
  2. TagsController: Rails controller for tag verification

    • Handles both GET and POST requests for the check action
    • Verifies the validity of the tag based on the QR code
  3. check.html.erb: View for the check page

    • Displays a button to scan QR codes
    • Shows the result of the scan

How to Use

  1. Navigate to the check page in the app
  2. Click the "Escanear Código QR" button
  3. The native camera will open
  4. Scan a QR code
  5. The result will be displayed on the page

Technical Notes

  • Uses Hotwire Native 1.2.0 for communication between the web view and native code
  • Uses ZXing for QR code scanning
  • The QR code should contain the tag code that is stored in the database
  • Communication between native code and web view is done via JavaScript evaluation
  • Form submission is used to post the QR code to the Rails check route
  • Uses delayed initialization to ensure the WebView is fully loaded
  • Implements recursive WebView finding in the view hierarchy
  • Sets a flag and dispatches a custom event to indicate that HotwireNative is available
  • Includes retry logic and better error messages

Permissions

The app requires the following permissions:

  • android.permission.CAMERA for camera access
  • android.hardware.camera feature declaration

These permissions are already included in the AndroidManifest.xml file.

package com.fireho.queroir
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.View
import android.webkit.JavascriptInterface
import android.webkit.WebView
import androidx.activity.enableEdgeToEdge
import dev.hotwire.navigation.activities.HotwireActivity
import dev.hotwire.navigation.navigator.NavigatorConfiguration
import dev.hotwire.navigation.util.applyDefaultImeWindowInsets
class MainActivity : HotwireActivity() {
private lateinit var qrCodeScanner: QrCodeScanner
private var webView: WebView? = null
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<View>(R.id.main_nav_host).applyDefaultImeWindowInsets()
// Initialize QR code scanner
qrCodeScanner = QrCodeScanner(this)
// Delay setup to ensure WebView is loaded
Handler(Looper.getMainLooper()).postDelayed({
findWebViewAndSetupInterface()
}, 1000)
}
/**
* Find WebView and set up JavaScript interface
*/
private fun findWebViewAndSetupInterface() {
try {
// Find the WebView
webView = findWebView()
if (webView != null) {
Log.d("MainActivity", "WebView found, setting up JavaScript interface")
setupJavaScriptInterface(webView!!)
} else {
Log.e("MainActivity", "WebView not found, retrying in 1 second")
// Retry after a delay
Handler(Looper.getMainLooper()).postDelayed({
findWebViewAndSetupInterface()
}, 1000)
}
} catch (e: Exception) {
Log.e("MainActivity", "Error finding WebView", e)
}
}
/**
* Set up JavaScript interface for QR code scanning
*/
private fun setupJavaScriptInterface(webView: WebView) {
try {
Log.d("MainActivity", "Setting up JavaScript interface")
// Add JavaScript interface
webView.addJavascriptInterface(object {
/**
* Perform an action from JavaScript
*/
@JavascriptInterface
fun perform(action: String) {
Log.d("MainActivity", "perform action received: $action")
if (action == "scanQrCode") {
runOnUiThread {
qrCodeScanner.scan()
}
}
}
}, "HotwireNative")
// Inject JavaScript to set up the callbacks system
val jsSetup = """
(function() {
// Set up HotwireNative object if it doesn't exist
window.HotwireNative = window.HotwireNative || {};
// Set a flag to indicate that HotwireNative is available
window.HotwireNative.isAvailable = true;
console.log('Setting up HotwireNative JavaScript interface');
// Initialize callbacks object if it doesn't exist
if (!window.HotwireNative.callbacks) {
window.HotwireNative.callbacks = {};
}
// Add registerCallback function if it doesn't exist
if (typeof window.HotwireNative.registerCallback !== 'function') {
window.HotwireNative.registerCallback = function(name, callback) {
window.HotwireNative.callbacks[name] = callback;
console.log('Registered callback: ' + name);
};
}
console.log('HotwireNative JavaScript interface setup complete');
// Dispatch an event to notify the web app that HotwireNative is available
var event = new CustomEvent('hotwireNativeAvailable');
document.dispatchEvent(event);
})();
""".trimIndent()
webView.evaluateJavascript(jsSetup) { result ->
Log.d("MainActivity", "JavaScript setup result: $result")
}
Log.d("MainActivity", "JavaScript interface setup complete")
} catch (e: Exception) {
Log.e("MainActivity", "Error setting up JavaScript interface", e)
}
}
/**
* Find the WebView in the view hierarchy
*/
private fun findWebView(): WebView? {
try {
// Try to find WebView in the navigator host
val navHost = findViewById<View>(R.id.main_nav_host)
// Log the view hierarchy for debugging
Log.d("MainActivity", "NavHost: $navHost")
// Try different approaches to find the WebView
// Approach 1: Direct child
val webView1 = navHost?.findViewById<WebView>(android.R.id.content)
if (webView1 != null) {
Log.d("MainActivity", "WebView found using approach 1")
return webView1
}
// Approach 2: Search all children recursively
val webView2 = findWebViewInViewHierarchy(navHost)
if (webView2 != null) {
Log.d("MainActivity", "WebView found using approach 2")
return webView2
}
Log.e("MainActivity", "WebView not found in view hierarchy")
return null
} catch (e: Exception) {
Log.e("MainActivity", "Error finding WebView", e)
return null
}
}
/**
* Find WebView in view hierarchy recursively
*/
private fun findWebViewInViewHierarchy(view: View?): WebView? {
if (view == null) return null
if (view is WebView) return view
try {
if (view is android.view.ViewGroup) {
for (i in 0 until view.childCount) {
val child = view.getChildAt(i)
val webView = findWebViewInViewHierarchy(child)
if (webView != null) return webView
}
}
} catch (e: Exception) {
Log.e("MainActivity", "Error searching view hierarchy", e)
}
return null
}
override fun navigatorConfigurations() = listOf(
NavigatorConfiguration(
name = "main",
// startLocation = "https://hotwire-native-demo.dev",
startLocation = "https://queroir.ai",
navigatorHostId = R.id.main_nav_host
)
)
// Handle QR code scan result
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// Get the current WebView
val currentWebView = webView
if (currentWebView != null) {
qrCodeScanner.handleScanResult(requestCode, resultCode, data, currentWebView)
} else {
Log.e("MainActivity", "No WebView available to handle QR code result")
// Try to find WebView again
val foundWebView = findWebView()
if (foundWebView != null) {
qrCodeScanner.handleScanResult(requestCode, resultCode, data, foundWebView)
} else {
Log.e("MainActivity", "Still no WebView available to handle QR code result")
}
}
}
}
import { Controller } from '@hotwired/stimulus';
// Connects to data-controller="qr-code-scanner"
export default class extends Controller {
static targets = ['result', 'status'];
isNativeApp = false;
connect() {
console.log('QR code scanner controller connected');
// Check if running in native app
this.checkNativeApp();
// Register callback for QR code scan result
this.registerCallback();
// Make handleScanResult available globally for iOS
window.handleQrCodeScanResult = this.handleScanResult.bind(this);
// Listen for hotwireNativeAvailable event
document.addEventListener('hotwireNativeAvailable', this.onHotwireNativeAvailable.bind(this));
}
disconnect() {
console.log('QR code scanner controller disconnected');
document.removeEventListener('hotwireNativeAvailable', this.onHotwireNativeAvailable.bind(this));
if (window.handleQrCodeScanResult) {
delete window.handleQrCodeScanResult;
}
}
// Called when HotwireNative becomes available
onHotwireNativeAvailable(event) {
console.log('HotwireNative is now available event received');
// A small delay might be needed for the interface to be fully ready
setTimeout(() => {
this.checkNativeApp();
this.registerCallback();
}, 100);
}
// Check if running in native app
checkNativeApp() {
// Check for Android and iOS native interfaces
if (
(window.HotwireNative && typeof window.HotwireNative.perform === 'function') ||
(window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.qrCodeScanned)
) {
console.log('Running in native app');
this.isNativeApp = true;
} else {
console.log('Not running in native app');
this.isNativeApp = false;
}
}
registerCallback() {
console.log('Attempting to register QR code scan callback');
// Android
if (window.HotwireNative && typeof window.HotwireNative.registerCallback === 'function') {
window.HotwireNative.registerCallback('qrCodeScanned', this.handleScanResult.bind(this));
console.log('QR code scan callback registered successfully for Android.');
// iOS
} else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.qrCodeScanned) {
console.log('iOS native interface detected. Callback will be handled by global function.');
} else {
console.warn('Native callback registration is not available.');
}
}
scan() {
console.log('Scan button clicked');
this.checkNativeApp();
if (this.isNativeApp) {
// Android
if (window.HotwireNative && typeof window.HotwireNative.perform === 'function') {
console.log('Calling HotwireNative.perform("scanQrCode")');
window.HotwireNative.perform('scanQrCode');
// iOS
} else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.qrCodeScanned) {
console.log('Calling iOS QR code scanner');
window.webkit.messageHandlers.qrCodeScanned.postMessage({});
}
} else {
console.log('Not in native app, cannot scan.');
if (window.navigator.userAgent.includes('Android') || window.navigator.userAgent.includes('iPhone')) {
this.showStatus('Scanner de QR code não disponível. Por favor, reinicie o aplicativo.', 'error');
} else {
this.showStatus('O scanner de QR code só está disponível no aplicativo móvel.', 'info');
}
}
}
// Handle scan result from native app
handleScanResult(result) {
console.log('Handling scan result:', result);
if (!result || !result.qr_code) {
console.error('Invalid QR code scan result', result);
this.showStatus('Código QR inválido ou não reconhecido.', 'error');
return;
}
// Show the QR code in the UI
this.showStatus(`Código QR escaneado: ${result.qr_code}`, 'info');
// Submit the QR code to the server
this.submitQrCode(result.qr_code);
}
// Submit QR code to server
submitQrCode(qrCode) {
console.log('Submitting QR code to server:', qrCode);
fetch('/tags/check', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content,
},
body: JSON.stringify({ qr_code: qrCode }),
})
.then((response) => {
console.log('Server response:', response);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then((data) => {
console.log('Server data:', data);
if (data.valid) {
// Show success message
this.showStatus(`Tag '${qrCode}' válido!`, 'success');
} else {
// Show error message
this.showStatus(`Tag '${qrCode}' não encontrado.`, 'error');
}
})
.catch((error) => {
console.error('Error checking QR code:', error);
this.showStatus('Erro ao verificar o código QR.', 'error');
});
}
// Show status message
showStatus(message, type) {
console.log('Showing status:', message, type);
// Create status element if it doesn't exist
if (!document.getElementById('status-message')) {
const statusDiv = document.createElement('div');
statusDiv.id = 'status-message';
statusDiv.className = 'py-2 px-3 mb-5 font-medium rounded-md inline-block';
document.querySelector('.w-full')?.prepend(statusDiv);
}
const statusElement = document.getElementById('status-message');
if (statusElement) {
statusElement.textContent = message;
// Set appropriate class based on message type
if (type === 'success') {
statusElement.className = 'py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block';
} else if (type === 'error') {
statusElement.className = 'py-2 px-3 bg-red-50 mb-5 text-red-500 font-medium rounded-md inline-block';
} else {
statusElement.className = 'py-2 px-3 bg-blue-50 mb-5 text-blue-500 font-medium rounded-md inline-block';
}
// Make sure the element is visible
statusElement.style.display = 'block';
} else {
console.error('Status element not found');
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment