Initial commit. Basic app.
This commit is contained in:
commit
63909abf6d
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Gradle
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Android Studio / IntelliJ
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
local.properties
|
||||||
|
captures/
|
||||||
|
|
||||||
|
# Android build artifacts
|
||||||
|
*.apk
|
||||||
|
*.aab
|
||||||
|
*.ap_
|
||||||
|
*.dex
|
||||||
|
|
||||||
|
# Native build
|
||||||
|
.externalNativeBuild/
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Keystore (never commit signing keys)
|
||||||
|
*.jks
|
||||||
|
*.keystore
|
||||||
|
release.properties
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
75
CLAUDE.md
Normal file
75
CLAUDE.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# JoykooScanner — Android Barcode Scanner Kiosk App
|
||||||
|
|
||||||
|
## What This Is
|
||||||
|
|
||||||
|
Native Android app (Kotlin) that wraps `https://smart.joykoo.mx/` in a fullscreen WebView for dedicated Chinese industrial handheld barcode scanners. The scanner operates in HID/keyboard-wedge mode — it sends Android key events like a physical keyboard.
|
||||||
|
|
||||||
|
## Critical Design Decision
|
||||||
|
|
||||||
|
The Android soft keyboard must NEVER appear. The custom `NoSoftKeyboardWebView` returns `null` from `onCreateInputConnection()` to prevent the IME from ever being invoked, while hardware key events from the scanner still flow through the standard View key-event dispatch chain. Do not revert this or add an InputConnection — it is the core of the app.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
app/src/main/java/mx/joykoo/scanner/
|
||||||
|
AppConfig.kt — URL, allowed hosts, gesture thresholds (single config point)
|
||||||
|
MainActivity.kt — Fullscreen kiosk activity, WebView setup, navigation, lifecycle
|
||||||
|
NoSoftKeyboardWebView.kt — IME suppression via null InputConnection
|
||||||
|
ScannerIntegration.kt — Interface stub for future vendor scanner SDK
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
- **Language**: Kotlin, targeting JVM 1.8 bytecode
|
||||||
|
- **SDK**: compileSdk 34, minSdk 24, targetSdk 34
|
||||||
|
- **Dependencies**: androidx core-ktx, appcompat, webkit, swiperefreshlayout
|
||||||
|
- **Gradle**: 8.7 with Kotlin DSL, AGP 8.5.2, Kotlin 1.9.24
|
||||||
|
- **Package**: `mx.joykoo.scanner`
|
||||||
|
|
||||||
|
### Build prerequisites (not yet installed on this server)
|
||||||
|
|
||||||
|
1. `sudo dnf install java-17-openjdk-devel` — only the JRE is installed, need javac
|
||||||
|
2. Android SDK cmdline-tools — download to `~/Android/Sdk/`, then install:
|
||||||
|
- `platform-tools`
|
||||||
|
- `platforms;android-34`
|
||||||
|
- `build-tools;34.0.0`
|
||||||
|
3. Set env vars:
|
||||||
|
```
|
||||||
|
JAVA_HOME=/usr/lib/jvm/java-17-openjdk-17.0.20.1.1-1.2.el9.x86_64
|
||||||
|
ANDROID_HOME=~/Android/Sdk
|
||||||
|
```
|
||||||
|
4. `./gradlew assembleDebug`
|
||||||
|
|
||||||
|
### Server environment (RHEL 9.8)
|
||||||
|
|
||||||
|
- JRE 17 installed at `/usr/lib/jvm/java-17-openjdk-17.0.20.1.1-1.2.el9.x86_64`
|
||||||
|
- 323 GB free on /home, 5.5 GB free on /
|
||||||
|
- curl, wget, unzip, zip available
|
||||||
|
- No Android SDK present anywhere
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All configurable values are in `AppConfig.kt`:
|
||||||
|
- `HOME_URL` — the webpage to load
|
||||||
|
- `ALLOWED_HOSTS` — domains that stay inside the WebView
|
||||||
|
- `BACK_PRESS_EXIT_COUNT` / `BACK_PRESS_WINDOW_MS` — back-button exit guard
|
||||||
|
- `ADMIN_TAP_COUNT` / `ADMIN_TAP_WINDOW_MS` — hidden admin exit gesture (7 taps top-left corner)
|
||||||
|
|
||||||
|
## Navigation Rules
|
||||||
|
|
||||||
|
- URLs on `joykoo.mx` or `smart.joykoo.mx` (and subdomains) load inside the WebView
|
||||||
|
- All other URLs are silently blocked
|
||||||
|
- Back button goes back in WebView history; 3 rapid presses exits when no history
|
||||||
|
- Admin exit: 7 rapid taps in top-left 60dp corner → confirmation dialog (PIN-ready stub)
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- HTTPS only, no cleartext, no SSL error bypass
|
||||||
|
- No JavaScript interfaces exposed
|
||||||
|
- No file:// or content:// access
|
||||||
|
- Network security config enforces system trust anchors only
|
||||||
|
- Domain allowlist in WebViewClient
|
||||||
|
|
||||||
|
## UI Strings
|
||||||
|
|
||||||
|
Spanish (`es`) — this is for a Mexico-based company.
|
||||||
91
README.md
Normal file
91
README.md
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# Joykoo Scanner
|
||||||
|
|
||||||
|
Aplicación nativa de Android que funciona como navegador dedicado de pantalla completa para el sistema web de Joykoo (`smart.joykoo.mx`), diseñada para escáneres industriales de códigos de barras y QR.
|
||||||
|
|
||||||
|
## Características
|
||||||
|
|
||||||
|
- **WebView en pantalla completa** — Carga el sitio web de la empresa sin barra de direcciones ni controles del navegador
|
||||||
|
- **Supresión del teclado virtual** — El teclado en pantalla de Android nunca aparece, mientras que la entrada del escáner de códigos de barras (modo HID/teclado) funciona normalmente
|
||||||
|
- **Modo kiosco** — Pantalla siempre encendida, modo inmersivo, navegación restringida al dominio de la empresa
|
||||||
|
- **Persistencia de sesión** — Cookies y almacenamiento local persisten entre reinicios de la aplicación
|
||||||
|
- **Manejo de errores de red** — Pantalla de reintento cuando no hay conexión Wi-Fi
|
||||||
|
- **Salida protegida** — Se requieren 3 pulsaciones rápidas del botón Atrás para salir; gesto oculto de administrador (7 toques en esquina superior izquierda) con diálogo de confirmación
|
||||||
|
|
||||||
|
## Requisitos
|
||||||
|
|
||||||
|
- Android 7.0+ (API 24)
|
||||||
|
- Dispositivo con escáner de códigos de barras integrado en modo HID/teclado
|
||||||
|
- Conexión Wi-Fi al servidor de la empresa
|
||||||
|
|
||||||
|
## Compilación
|
||||||
|
|
||||||
|
### Prerrequisitos
|
||||||
|
|
||||||
|
- JDK 17
|
||||||
|
- Android SDK con:
|
||||||
|
- `platform-tools`
|
||||||
|
- `platforms;android-34`
|
||||||
|
- `build-tools;34.0.0`
|
||||||
|
|
||||||
|
### Usando Android Studio
|
||||||
|
|
||||||
|
Abrir el proyecto y compilar normalmente.
|
||||||
|
|
||||||
|
### Línea de comandos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export JAVA_HOME=/ruta/a/jdk-17
|
||||||
|
export ANDROID_HOME=/ruta/a/android-sdk
|
||||||
|
|
||||||
|
./gradlew assembleDebug # APK de depuración
|
||||||
|
./gradlew assembleRelease # APK de producción (requiere configurar firma)
|
||||||
|
```
|
||||||
|
|
||||||
|
El APK resultante se encuentra en `app/build/outputs/apk/`.
|
||||||
|
|
||||||
|
## Configuración
|
||||||
|
|
||||||
|
Todos los valores configurables están en un solo archivo:
|
||||||
|
|
||||||
|
**`app/src/main/java/mx/joykoo/scanner/AppConfig.kt`**
|
||||||
|
|
||||||
|
| Parámetro | Descripción | Valor por defecto |
|
||||||
|
|---|---|---|
|
||||||
|
| `HOME_URL` | URL que se carga al iniciar | `https://smart.joykoo.mx/` |
|
||||||
|
| `ALLOWED_HOSTS` | Dominios permitidos dentro del WebView | `smart.joykoo.mx`, `joykoo.mx` |
|
||||||
|
| `BACK_PRESS_EXIT_COUNT` | Pulsaciones rápidas de Atrás para salir | 3 |
|
||||||
|
| `ADMIN_TAP_COUNT` | Toques en esquina superior izquierda para menú de administrador | 7 |
|
||||||
|
|
||||||
|
## Uso del escáner
|
||||||
|
|
||||||
|
1. El usuario toca un campo de texto en la página web
|
||||||
|
2. El campo recibe el foco — **el teclado virtual no aparece**
|
||||||
|
3. El usuario escanea un código de barras o QR
|
||||||
|
4. El texto escaneado aparece en el campo
|
||||||
|
5. Si el escáner envía Enter al final, la página lo procesa normalmente
|
||||||
|
|
||||||
|
## Estructura del proyecto
|
||||||
|
|
||||||
|
```
|
||||||
|
app/src/main/java/mx/joykoo/scanner/
|
||||||
|
├── AppConfig.kt — Configuración centralizada
|
||||||
|
├── MainActivity.kt — Actividad principal (kiosco, WebView, navegación)
|
||||||
|
├── NoSoftKeyboardWebView.kt — WebView que suprime el teclado virtual
|
||||||
|
└── ScannerIntegration.kt — Interfaz para futura integración con SDK de escáner
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integración futura con SDK de escáner
|
||||||
|
|
||||||
|
La aplicación funciona inmediatamente con escáneres en modo HID/teclado. Para escáneres que requieren integración con SDK propietario (broadcast intents), implementar la interfaz `ScannerIntegration` y registrar el BroadcastReceiver correspondiente.
|
||||||
|
|
||||||
|
## Seguridad
|
||||||
|
|
||||||
|
- Solo conexiones HTTPS
|
||||||
|
- No se omiten errores de certificado TLS
|
||||||
|
- No se exponen interfaces JavaScript
|
||||||
|
- Acceso a archivos y contenido deshabilitado
|
||||||
|
- Navegación restringida a dominios autorizados
|
||||||
|
|
||||||
|
## Licencia
|
||||||
|
|
||||||
|
Uso interno — Joykoo.
|
||||||
44
app/build.gradle.kts
Normal file
44
app/build.gradle.kts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "mx.joykoo.scanner"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "mx.joykoo.scanner"
|
||||||
|
minSdk = 24
|
||||||
|
targetSdk = 34
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("androidx.core:core-ktx:1.13.1")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||||
|
implementation("androidx.webkit:webkit:1.11.0")
|
||||||
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
|
}
|
||||||
11
app/proguard-rules.pro
vendored
Normal file
11
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Keep WebView JavaScript interface methods
|
||||||
|
-keepclassmembers class * {
|
||||||
|
@android.webkit.JavascriptInterface <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep WebViewClient and WebChromeClient callbacks
|
||||||
|
-keepclassmembers class * extends android.webkit.WebViewClient {
|
||||||
|
public void *(android.webkit.WebView, java.lang.String);
|
||||||
|
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
|
||||||
|
public boolean *(android.webkit.WebView, java.lang.String);
|
||||||
|
}
|
||||||
42
app/src/main/AndroidManifest.xml
Normal file
42
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.JoykooScanner"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:usesCleartextTraffic="false">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
windowSoftInputMode="stateAlwaysHidden" is a first layer of defense:
|
||||||
|
it tells the system not to show the IME when the activity starts or
|
||||||
|
resumes. However, WebView can still explicitly request the IME when
|
||||||
|
an HTML input is focused, so this alone is NOT sufficient.
|
||||||
|
The real suppression happens in NoSoftKeyboardWebView which overrides
|
||||||
|
onCreateInputConnection() to return null, preventing the IME from
|
||||||
|
ever being invoked while still allowing hardware key events to flow
|
||||||
|
through the standard View key-event dispatch.
|
||||||
|
-->
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:configChanges="orientation|screenSize|keyboardHidden|keyboard|screenLayout|smallestScreenSize"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
26
app/src/main/java/mx/joykoo/scanner/AppConfig.kt
Normal file
26
app/src/main/java/mx/joykoo/scanner/AppConfig.kt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package mx.joykoo.scanner
|
||||||
|
|
||||||
|
object AppConfig {
|
||||||
|
/** The URL loaded on app startup. Change this single value to point to a different site. */
|
||||||
|
const val HOME_URL = "https://smart.joykoo.mx/"
|
||||||
|
|
||||||
|
/** Hostnames that are considered part of the company application.
|
||||||
|
* Navigation to URLs on these hosts stays inside the WebView.
|
||||||
|
* All other hosts are blocked (or optionally opened externally). */
|
||||||
|
val ALLOWED_HOSTS = setOf(
|
||||||
|
"smart.joykoo.mx",
|
||||||
|
"joykoo.mx",
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Number of rapid Back presses required to exit the app when there is no WebView history. */
|
||||||
|
const val BACK_PRESS_EXIT_COUNT = 3
|
||||||
|
|
||||||
|
/** Time window (ms) in which BACK_PRESS_EXIT_COUNT presses must occur to exit. */
|
||||||
|
const val BACK_PRESS_WINDOW_MS = 2000L
|
||||||
|
|
||||||
|
/** Number of taps on the hidden admin area required to trigger the admin exit. */
|
||||||
|
const val ADMIN_TAP_COUNT = 7
|
||||||
|
|
||||||
|
/** Time window (ms) for admin tap sequence. */
|
||||||
|
const val ADMIN_TAP_WINDOW_MS = 3000L
|
||||||
|
}
|
||||||
371
app/src/main/java/mx/joykoo/scanner/MainActivity.kt
Normal file
371
app/src/main/java/mx/joykoo/scanner/MainActivity.kt
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
package mx.joykoo.scanner
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import android.view.WindowInsetsController
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.ValueCallback
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
|
import android.webkit.WebResourceError
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebSettings
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var webView: NoSoftKeyboardWebView
|
||||||
|
private lateinit var errorOverlay: View
|
||||||
|
private var fileUploadCallback: ValueCallback<Array<Uri>>? = null
|
||||||
|
private var isShowingError = false
|
||||||
|
|
||||||
|
// Admin exit: rapid taps in the top-left corner
|
||||||
|
private var adminTapTimestamps = mutableListOf<Long>()
|
||||||
|
|
||||||
|
// Back-press exit: rapid presses when no WebView history
|
||||||
|
private var backPressTimestamps = mutableListOf<Long>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
keepScreenAwake()
|
||||||
|
enterImmersiveMode()
|
||||||
|
setupWebView()
|
||||||
|
setupErrorOverlay()
|
||||||
|
setupBackNavigation()
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
webView.loadUrl(AppConfig.HOME_URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureWebViewFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Fullscreen / Immersive ──────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun keepScreenAwake() {
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enterImmersiveMode() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
window.insetsController?.let { controller ->
|
||||||
|
controller.hide(WindowInsets.Type.systemBars())
|
||||||
|
controller.systemBarsBehavior =
|
||||||
|
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
window.decorView.systemUiVisibility = (
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
or View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||||
|
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||||
|
super.onWindowFocusChanged(hasFocus)
|
||||||
|
if (hasFocus) enterImmersiveMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WebView Setup ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
private fun setupWebView() {
|
||||||
|
webView = findViewById(R.id.webview)
|
||||||
|
|
||||||
|
webView.settings.apply {
|
||||||
|
javaScriptEnabled = true
|
||||||
|
domStorageEnabled = true
|
||||||
|
databaseEnabled = true
|
||||||
|
|
||||||
|
// Allow mixed content only if absolutely necessary for the target site.
|
||||||
|
// Default to the safest option.
|
||||||
|
mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
|
||||||
|
|
||||||
|
// Cache and storage for session persistence
|
||||||
|
cacheMode = WebSettings.LOAD_DEFAULT
|
||||||
|
setSupportMultipleWindows(false)
|
||||||
|
|
||||||
|
// Reasonable user-agent so the target site serves a normal mobile page
|
||||||
|
userAgentString = "$userAgentString JoykooScanner/1.0"
|
||||||
|
|
||||||
|
// Allow file access from file: URLs only (not content:// or broad access)
|
||||||
|
allowFileAccess = false
|
||||||
|
allowContentAccess = false
|
||||||
|
|
||||||
|
// Viewport and zoom for industrial screen sizes
|
||||||
|
useWideViewPort = true
|
||||||
|
loadWithOverviewMode = true
|
||||||
|
builtInZoomControls = true
|
||||||
|
displayZoomControls = false
|
||||||
|
|
||||||
|
// Text encoding
|
||||||
|
defaultTextEncodingName = "UTF-8"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persistent cookies across app restarts
|
||||||
|
CookieManager.getInstance().apply {
|
||||||
|
setAcceptCookie(true)
|
||||||
|
setAcceptThirdPartyCookies(webView, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
webView.webViewClient = ScannerWebViewClient()
|
||||||
|
webView.webChromeClient = ScannerWebChromeClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Error Overlay ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun setupErrorOverlay() {
|
||||||
|
errorOverlay = findViewById(R.id.error_overlay)
|
||||||
|
findViewById<View>(R.id.btn_retry).setOnClickListener {
|
||||||
|
hideError()
|
||||||
|
if (isNetworkAvailable()) {
|
||||||
|
webView.reload()
|
||||||
|
} else {
|
||||||
|
showError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showError() {
|
||||||
|
isShowingError = true
|
||||||
|
errorOverlay.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideError() {
|
||||||
|
isShowingError = false
|
||||||
|
errorOverlay.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isNetworkAvailable(): Boolean {
|
||||||
|
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
val network = cm.activeNetwork ?: return false
|
||||||
|
val caps = cm.getNetworkCapabilities(network) ?: return false
|
||||||
|
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Back Navigation ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun setupBackNavigation() {
|
||||||
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
if (isShowingError) {
|
||||||
|
// Don't navigate away from error; just retry
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (webView.canGoBack()) {
|
||||||
|
webView.goBack()
|
||||||
|
backPressTimestamps.clear()
|
||||||
|
} else {
|
||||||
|
handleExitBackPress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleExitBackPress() {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
backPressTimestamps.add(now)
|
||||||
|
backPressTimestamps.removeAll { now - it > AppConfig.BACK_PRESS_WINDOW_MS }
|
||||||
|
|
||||||
|
if (backPressTimestamps.size >= AppConfig.BACK_PRESS_EXIT_COUNT) {
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
val remaining = AppConfig.BACK_PRESS_EXIT_COUNT - backPressTimestamps.size
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
"Presione Atrás $remaining vez/veces más para salir",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Admin Exit (hidden tap gesture) ─────────────────────────────────
|
||||||
|
|
||||||
|
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
||||||
|
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||||
|
// Top-left 60x60 dp region as the hidden admin area
|
||||||
|
val threshold = (60 * resources.displayMetrics.density).toInt()
|
||||||
|
if (ev.x < threshold && ev.y < threshold) {
|
||||||
|
handleAdminTap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.dispatchTouchEvent(ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleAdminTap() {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
adminTapTimestamps.add(now)
|
||||||
|
adminTapTimestamps.removeAll { now - it > AppConfig.ADMIN_TAP_WINDOW_MS }
|
||||||
|
|
||||||
|
if (adminTapTimestamps.size >= AppConfig.ADMIN_TAP_COUNT) {
|
||||||
|
adminTapTimestamps.clear()
|
||||||
|
showAdminExitDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showAdminExitDialog() {
|
||||||
|
// Structured so a PIN check can be added here before calling finish()
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle("Salir de la aplicación")
|
||||||
|
.setMessage("¿Desea cerrar la aplicación del escáner?")
|
||||||
|
.setPositiveButton("Salir") { _, _ -> finish() }
|
||||||
|
.setNegativeButton("Cancelar", null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lifecycle ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
webView.onResume()
|
||||||
|
enterImmersiveMode()
|
||||||
|
ensureWebViewFocus()
|
||||||
|
CookieManager.getInstance().flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
webView.onPause()
|
||||||
|
CookieManager.getInstance().flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
webView.saveState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
|
webView.restoreState(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
webView.destroy()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hardware Key Events ─────────────────────────────────────────────
|
||||||
|
// Hardware key events from the barcode scanner flow through the
|
||||||
|
// standard Android dispatch: Activity → ViewGroup → focused View.
|
||||||
|
// The WebView handles them in its own dispatchKeyEvent/onKeyDown.
|
||||||
|
//
|
||||||
|
// We ensure the WebView has focus so it receives key events directly.
|
||||||
|
// No override of dispatchKeyEvent is needed — the default chain works
|
||||||
|
// correctly when the WebView is focused. Overriding it with a manual
|
||||||
|
// forward would cause double dispatch.
|
||||||
|
|
||||||
|
private fun ensureWebViewFocus() {
|
||||||
|
if (::webView.isInitialized && !webView.hasFocus()) {
|
||||||
|
webView.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WebViewClient ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private inner class ScannerWebViewClient : WebViewClient() {
|
||||||
|
|
||||||
|
override fun shouldOverrideUrlLoading(
|
||||||
|
view: WebView,
|
||||||
|
request: WebResourceRequest
|
||||||
|
): Boolean {
|
||||||
|
val host = request.url.host ?: return true
|
||||||
|
// Allow navigation within the company's domains
|
||||||
|
if (AppConfig.ALLOWED_HOSTS.any { host == it || host.endsWith(".$it") }) {
|
||||||
|
return false // let WebView handle it
|
||||||
|
}
|
||||||
|
// Block navigation to external sites
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
|
||||||
|
super.onPageStarted(view, url, favicon)
|
||||||
|
if (isShowingError) hideError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView, url: String?) {
|
||||||
|
super.onPageFinished(view, url)
|
||||||
|
CookieManager.getInstance().flush()
|
||||||
|
ensureWebViewFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceivedError(
|
||||||
|
view: WebView,
|
||||||
|
request: WebResourceRequest,
|
||||||
|
error: WebResourceError
|
||||||
|
) {
|
||||||
|
// Only show error overlay for main-frame navigation failures
|
||||||
|
if (request.isForMainFrame) {
|
||||||
|
showError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do NOT override onReceivedSslError to proceed — let TLS
|
||||||
|
// errors fail as they should for security.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WebChromeClient ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private inner class ScannerWebChromeClient : WebChromeClient() {
|
||||||
|
|
||||||
|
// File upload support: <input type="file"> in the webpage
|
||||||
|
override fun onShowFileChooser(
|
||||||
|
webView: WebView,
|
||||||
|
filePathCallback: ValueCallback<Array<Uri>>,
|
||||||
|
fileChooserParams: FileChooserParams
|
||||||
|
): Boolean {
|
||||||
|
fileUploadCallback?.onReceiveValue(null)
|
||||||
|
fileUploadCallback = filePathCallback
|
||||||
|
|
||||||
|
val intent = fileChooserParams.createIntent()
|
||||||
|
try {
|
||||||
|
startActivityForResult(intent, FILE_CHOOSER_REQUEST)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
fileUploadCallback?.onReceiveValue(null)
|
||||||
|
fileUploadCallback = null
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: android.content.Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
if (requestCode == FILE_CHOOSER_REQUEST) {
|
||||||
|
fileUploadCallback?.onReceiveValue(
|
||||||
|
if (resultCode == RESULT_OK && data != null) {
|
||||||
|
WebChromeClient.FileChooserParams.parseResult(resultCode, data)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
fileUploadCallback = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val FILE_CHOOSER_REQUEST = 1001
|
||||||
|
}
|
||||||
|
}
|
||||||
68
app/src/main/java/mx/joykoo/scanner/NoSoftKeyboardWebView.kt
Normal file
68
app/src/main/java/mx/joykoo/scanner/NoSoftKeyboardWebView.kt
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package mx.joykoo.scanner
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.view.inputmethod.InputConnection
|
||||||
|
import android.webkit.WebView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebView subclass that prevents the Android software keyboard (IME) from
|
||||||
|
* appearing while preserving hardware keyboard / barcode-scanner key events.
|
||||||
|
*
|
||||||
|
* HOW IT WORKS
|
||||||
|
* ============
|
||||||
|
* When an HTML <input> inside a WebView gains focus, the framework calls
|
||||||
|
* onCreateInputConnection() on the focused View. If this method returns a
|
||||||
|
* non-null InputConnection, the InputMethodManager (IMM) has a channel to
|
||||||
|
* the IME and will show the software keyboard.
|
||||||
|
*
|
||||||
|
* By returning null, we tell the IMM that this View does not participate in
|
||||||
|
* the input-method protocol. The IMM therefore never shows the soft keyboard
|
||||||
|
* for this View — it is prevented from opening in the first place rather
|
||||||
|
* than being hidden reactively after it appears.
|
||||||
|
*
|
||||||
|
* WHY HARDWARE/SCANNER INPUT STILL WORKS
|
||||||
|
* =======================================
|
||||||
|
* Hardware key events (from a physical keyboard or a barcode scanner in
|
||||||
|
* HID/keyboard-wedge mode) are delivered through the Android View key-event
|
||||||
|
* dispatch pipeline: Activity.dispatchKeyEvent → ViewGroup.dispatchKeyEvent
|
||||||
|
* → View.dispatchKeyEvent → View.onKeyDown/onKeyUp. This pipeline is
|
||||||
|
* completely independent of InputConnection/IME. WebView's internal
|
||||||
|
* key-event handling (which forwards events to the Chromium renderer and
|
||||||
|
* into focused HTML inputs) works through this pipeline, so hardware key
|
||||||
|
* events continue to reach whichever HTML input has focus inside the
|
||||||
|
* WebView.
|
||||||
|
*
|
||||||
|
* The only thing lost by returning null is the ability to receive composed
|
||||||
|
* text from a software IME (e.g. predictive text, swipe typing, CJK
|
||||||
|
* composition). Since this device is a dedicated barcode scanner with no
|
||||||
|
* need for software keyboard input, this is the desired behavior.
|
||||||
|
*
|
||||||
|
* ADDITIONAL SAFETY: onCheckIsTextEditor()
|
||||||
|
* ========================================
|
||||||
|
* Some Android IME implementations also check onCheckIsTextEditor() to
|
||||||
|
* decide whether to show. Returning false provides a second signal that
|
||||||
|
* this View does not want IME interaction.
|
||||||
|
*/
|
||||||
|
class NoSoftKeyboardWebView : WebView {
|
||||||
|
|
||||||
|
constructor(context: Context) : super(context)
|
||||||
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
|
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
|
||||||
|
super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
|
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
|
||||||
|
// Returning null prevents the IMM from establishing a connection to
|
||||||
|
// any software IME. The soft keyboard cannot appear because there is
|
||||||
|
// no InputConnection for it to communicate through. Hardware key
|
||||||
|
// events still flow via the View key-event dispatch chain.
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCheckIsTextEditor(): Boolean {
|
||||||
|
// Signal to the framework that this View is not a text editor,
|
||||||
|
// preventing IME-related behaviors from triggering.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/src/main/java/mx/joykoo/scanner/ScannerIntegration.kt
Normal file
30
app/src/main/java/mx/joykoo/scanner/ScannerIntegration.kt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package mx.joykoo.scanner
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension point for vendor-specific scanner SDK integration.
|
||||||
|
*
|
||||||
|
* Generic Chinese industrial scanners often provide a broadcast-based API
|
||||||
|
* (e.g. via Intent actions like "com.scanner.broadcast" or similar) in
|
||||||
|
* addition to or instead of HID keyboard-wedge mode.
|
||||||
|
*
|
||||||
|
* To integrate a vendor SDK:
|
||||||
|
* 1. Add the vendor library to app/build.gradle.kts dependencies.
|
||||||
|
* 2. Implement a concrete subclass of this interface.
|
||||||
|
* 3. Register/unregister the BroadcastReceiver in onResume/onPause.
|
||||||
|
* 4. When a barcode is received, call the provided callback to inject the
|
||||||
|
* scanned text into the WebView's focused input.
|
||||||
|
*
|
||||||
|
* The HID keyboard-wedge mode (hardware key events) works without any of
|
||||||
|
* this — it is handled automatically by Android's View key dispatch.
|
||||||
|
*/
|
||||||
|
interface ScannerIntegration {
|
||||||
|
|
||||||
|
/** Called when the activity resumes. Register broadcast receivers here. */
|
||||||
|
fun onResume()
|
||||||
|
|
||||||
|
/** Called when the activity pauses. Unregister broadcast receivers here. */
|
||||||
|
fun onPause()
|
||||||
|
|
||||||
|
/** Called when the activity is destroyed. Clean up resources. */
|
||||||
|
fun onDestroy()
|
||||||
|
}
|
||||||
10
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
10
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#1565C0"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
</vector>
|
||||||
51
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
51
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<!-- QR code icon outline -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M30,30h18v18h-18z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M60,30h18v18h-18z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M30,60h18v18h-18z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1A237E"
|
||||||
|
android:pathData="M33,33h12v12h-12z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1A237E"
|
||||||
|
android:pathData="M63,33h12v12h-12z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1A237E"
|
||||||
|
android:pathData="M33,63h12v12h-12z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1A237E"
|
||||||
|
android:pathData="M36,36h6v6h-6z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1A237E"
|
||||||
|
android:pathData="M66,36h6v6h-6z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1A237E"
|
||||||
|
android:pathData="M36,66h6v6h-6z" />
|
||||||
|
<!-- Data dots -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#1A237E"
|
||||||
|
android:pathData="M54,54h6v6h-6z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1A237E"
|
||||||
|
android:pathData="M60,60h6v6h-6z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1A237E"
|
||||||
|
android:pathData="M66,60h6v6h-6z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1A237E"
|
||||||
|
android:pathData="M54,66h6v6h-6z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1A237E"
|
||||||
|
android:pathData="M72,66h6v6h-6z" />
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/ic_launcher_legacy.xml
Normal file
5
app/src/main/res/drawable/ic_launcher_legacy.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<item android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</layer-list>
|
||||||
49
app/src/main/res/layout/activity_main.xml
Normal file
49
app/src/main/res/layout/activity_main.xml
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<mx.joykoo.scanner.NoSoftKeyboardWebView
|
||||||
|
android:id="@+id/webview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<!-- Error overlay shown when the page cannot load (e.g. no network) -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/error_overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#FFFFFF"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="32dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/error_no_connection"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/error_check_wifi"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textColor="#666666"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_retry"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:paddingHorizontal="32dp"
|
||||||
|
android:text="@string/retry" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 166 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 123 B |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 221 B |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 538 B |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 735 B |
7
app/src/main/res/values/strings.xml
Normal file
7
app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Joykoo Scanner</string>
|
||||||
|
<string name="error_no_connection">No se puede conectar</string>
|
||||||
|
<string name="error_check_wifi">Verifique la conexión Wi-Fi e intente de nuevo.</string>
|
||||||
|
<string name="retry">Reintentar</string>
|
||||||
|
</resources>
|
||||||
9
app/src/main/res/values/themes.xml
Normal file
9
app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.JoykooScanner" parent="Theme.AppCompat.NoActionBar">
|
||||||
|
<item name="android:windowFullscreen">true</item>
|
||||||
|
<item name="android:windowNoTitle">true</item>
|
||||||
|
<item name="android:windowActionBar">false</item>
|
||||||
|
<item name="android:windowBackground">@android:color/white</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
9
app/src/main/res/xml/network_security_config.xml
Normal file
9
app/src/main/res/xml/network_security_config.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<!-- Only allow HTTPS connections. No cleartext traffic. -->
|
||||||
|
<base-config cleartextTrafficPermitted="false">
|
||||||
|
<trust-anchors>
|
||||||
|
<certificates src="system" />
|
||||||
|
</trust-anchors>
|
||||||
|
</base-config>
|
||||||
|
</network-security-config>
|
||||||
4
build.gradle.kts
Normal file
4
build.gradle.kts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application") version "8.5.2" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "1.9.24" apply false
|
||||||
|
}
|
||||||
4
gradle.properties
Normal file
4
gradle.properties
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
android.useAndroidX=true
|
||||||
|
kotlin.code.style=official
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
133
gradlew
vendored
Executable file
133
gradlew
vendored
Executable file
@ -0,0 +1,133 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld -- "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NonStop* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1 ; then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is://undefined. That's://why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is://undefined. That's://why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stracks://the precedence://of values.
|
||||||
|
# For this to work, currentIFS://should remain default.
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xeli" mode is set and the command fails
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
stty -icanon min 1 time 0 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
19
settings.gradle.kts
Normal file
19
settings.gradle.kts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "JoykooScanner"
|
||||||
|
include(":app")
|
||||||
Loading…
Reference in New Issue
Block a user