feat: MVP implementation - Kiosk Browser
- Fullscreen WebView with immersive mode - URL whitelist system - Admin PIN-protected settings - Boot receiver for auto-launch - Idle timeout with homepage reset - Lock Task Mode support - Material Design 3 UI Team: Pluto (coordinator), Margaret, Spirit
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
@@ -1,26 +1,70 @@
|
|||||||
# Android Kiosk Browser
|
# Android Kiosk Browser
|
||||||
|
|
||||||
A customizable Android browser application designed for kiosk and digital signage deployments.
|
A customizable locked-down browser for Android kiosk and digital signage deployments.
|
||||||
|
|
||||||
## Features (Planned)
|
## Features (MVP)
|
||||||
|
|
||||||
- **Restricted Browsing**: Lock to specific URLs or domains
|
- **Fullscreen WebView** - Immersive mode, no address bar or browser chrome
|
||||||
- **Kiosk Mode**: Full-screen immersive experience
|
- **URL Whitelist** - Only allow configured domains
|
||||||
- **Admin Interface**: Remote configuration management
|
- **Lock Task Mode** - Disables home/back/recent buttons
|
||||||
- **Offline Support**: Cache pages for offline viewing
|
- **Admin PIN Exit** - PIN-protected escape from kiosk mode
|
||||||
- **Auto-refresh**: Periodic page refresh for digital signage
|
- **Auto-launch on Boot** - Starts automatically when device powers on
|
||||||
|
- **Idle Timeout** - Returns to homepage after inactivity
|
||||||
|
- **Admin Settings** - Configure whitelist, homepage, timeout, PIN
|
||||||
|
|
||||||
## Project Status
|
## Tech Stack
|
||||||
|
|
||||||
🚧 **In Development**
|
- Kotlin
|
||||||
|
- Android WebView
|
||||||
|
- Lock Task Mode (API 21+)
|
||||||
|
- DataStore Preferences
|
||||||
|
- Material Design 3
|
||||||
|
|
||||||
## Getting Started
|
## Requirements
|
||||||
|
|
||||||
TBD
|
- Android 5.0+ (API 21)
|
||||||
|
- For Lock Task Mode: Device Owner provisioning
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew assembleDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Device Setup
|
||||||
|
|
||||||
|
To enable Lock Task Mode (full kiosk lockdown):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb shell dpm set-device-owner farm.hobbs.kioskbrowser/.KioskDeviceAdminReceiver
|
||||||
|
```
|
||||||
|
|
||||||
|
## Admin Access
|
||||||
|
|
||||||
|
Triple-tap the top-left corner of the screen to access admin settings.
|
||||||
|
|
||||||
|
Default PIN: `1234`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Settings available in Admin panel:
|
||||||
|
- Homepage URL
|
||||||
|
- Allowed domains (comma-separated)
|
||||||
|
- Idle timeout (seconds)
|
||||||
|
- Admin PIN
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- [ ] Remote configuration
|
||||||
|
- [ ] Multiple profiles
|
||||||
|
- [ ] Usage analytics
|
||||||
|
- [ ] Digital signage mode (URL rotation)
|
||||||
|
- [ ] Session data clearing on idle
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
TBD
|
MIT License - hobbs.farm
|
||||||
|
|
||||||
---
|
---
|
||||||
*Project initialized: 2026-02-03*
|
|
||||||
|
*Project managed in Vikunja | Docs on Wiki*
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "farm.hobbs.kioskbrowser"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "farm.hobbs.kioskbrowser"
|
||||||
|
minSdk = 21
|
||||||
|
targetSdk = 34
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "1.0.0-mvp"
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.appcompat)
|
||||||
|
implementation(libs.material)
|
||||||
|
implementation(libs.androidx.activity)
|
||||||
|
implementation(libs.androidx.constraintlayout)
|
||||||
|
implementation(libs.androidx.webkit)
|
||||||
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.KioskBrowser"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
tools:targetApi="31">
|
||||||
|
|
||||||
|
<!-- Main Kiosk Activity -->
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="landscape"
|
||||||
|
android:configChanges="orientation|screenSize|keyboard|keyboardHidden"
|
||||||
|
android:theme="@style/Theme.KioskBrowser.Fullscreen">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.HOME" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<!-- Admin Settings Activity -->
|
||||||
|
<activity
|
||||||
|
android:name=".AdminActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@style/Theme.KioskBrowser" />
|
||||||
|
|
||||||
|
<!-- Boot Receiver - Auto-launch on device start -->
|
||||||
|
<receiver
|
||||||
|
android:name=".BootReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
android:enabled="true">
|
||||||
|
<intent-filter android:priority="999">
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<!-- Device Admin Receiver for Lock Task Mode -->
|
||||||
|
<receiver
|
||||||
|
android:name=".KioskDeviceAdminReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.BIND_DEVICE_ADMIN">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.device_admin"
|
||||||
|
android:resource="@xml/device_admin" />
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package farm.hobbs.kioskbrowser
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import farm.hobbs.kioskbrowser.databinding.ActivityAdminBinding
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import android.text.InputType
|
||||||
|
import android.widget.EditText
|
||||||
|
|
||||||
|
class AdminActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityAdminBinding
|
||||||
|
private val adminPin = "1234" // Default PIN - should be configurable
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Require PIN before showing settings
|
||||||
|
showPinDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPinDialog() {
|
||||||
|
val input = EditText(this).apply {
|
||||||
|
inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
|
||||||
|
hint = "Enter Admin PIN"
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle("Admin Access")
|
||||||
|
.setMessage("Enter PIN to access settings")
|
||||||
|
.setView(input)
|
||||||
|
.setPositiveButton("OK") { _, _ ->
|
||||||
|
if (input.text.toString() == getStoredPin()) {
|
||||||
|
setupUI()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, "Invalid PIN", Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton("Cancel") { _, _ -> finish() }
|
||||||
|
.setCancelable(false)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getStoredPin(): String {
|
||||||
|
return runBlocking {
|
||||||
|
dataStore.data.first()[stringPreferencesKey("admin_pin")] ?: adminPin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupUI() {
|
||||||
|
binding = ActivityAdminBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
loadCurrentSettings()
|
||||||
|
|
||||||
|
binding.btnSave.setOnClickListener {
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnExit.setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnExitKiosk.setOnClickListener {
|
||||||
|
// Exit kiosk mode completely
|
||||||
|
finishAffinity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadCurrentSettings() {
|
||||||
|
runBlocking {
|
||||||
|
dataStore.data.first().let { prefs ->
|
||||||
|
binding.etHomepage.setText(
|
||||||
|
prefs[stringPreferencesKey("homepage")] ?: "https://hobbs.farm"
|
||||||
|
)
|
||||||
|
binding.etWhitelist.setText(
|
||||||
|
prefs[stringPreferencesKey("whitelist")] ?: "hobbs.farm"
|
||||||
|
)
|
||||||
|
binding.etIdleTimeout.setText(
|
||||||
|
prefs[stringPreferencesKey("idle_timeout")] ?: "300"
|
||||||
|
)
|
||||||
|
binding.etAdminPin.setText(
|
||||||
|
prefs[stringPreferencesKey("admin_pin")] ?: "1234"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveSettings() {
|
||||||
|
runBlocking {
|
||||||
|
dataStore.edit { prefs ->
|
||||||
|
prefs[stringPreferencesKey("homepage")] = binding.etHomepage.text.toString()
|
||||||
|
prefs[stringPreferencesKey("whitelist")] = binding.etWhitelist.text.toString()
|
||||||
|
prefs[stringPreferencesKey("idle_timeout")] = binding.etIdleTimeout.text.toString()
|
||||||
|
prefs[stringPreferencesKey("admin_pin")] = binding.etAdminPin.text.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Toast.makeText(this, "Settings saved", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package farm.hobbs.kioskbrowser
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action == Intent.ACTION_BOOT_COMPLETED ||
|
||||||
|
intent.action == "android.intent.action.QUICKBOOT_POWERON") {
|
||||||
|
|
||||||
|
val launchIntent = Intent(context, MainActivity::class.java).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
|
}
|
||||||
|
context.startActivity(launchIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package farm.hobbs.kioskbrowser
|
||||||
|
|
||||||
|
import android.app.admin.DeviceAdminReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
|
class KioskDeviceAdminReceiver : DeviceAdminReceiver() {
|
||||||
|
override fun onEnabled(context: Context, intent: Intent) {
|
||||||
|
super.onEnabled(context, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisabled(context: Context, intent: Intent) {
|
||||||
|
super.onDisabled(context, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
package farm.hobbs.kioskbrowser
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.admin.DevicePolicyManager
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import android.view.WindowInsetsController
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import farm.hobbs.kioskbrowser.databinding.ActivityMainBinding
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
val Context.dataStore by preferencesDataStore(name = "kiosk_settings")
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
private lateinit var webView: WebView
|
||||||
|
|
||||||
|
private val idleHandler = Handler(Looper.getMainLooper())
|
||||||
|
private var idleTimeoutMs = 300_000L // 5 minutes default
|
||||||
|
private var homepage = "https://hobbs.farm"
|
||||||
|
private var whitelist = listOf("hobbs.farm")
|
||||||
|
|
||||||
|
private val idleRunnable = Runnable {
|
||||||
|
loadHomepage()
|
||||||
|
clearSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
setupFullscreen()
|
||||||
|
loadSettings()
|
||||||
|
setupWebView()
|
||||||
|
loadHomepage()
|
||||||
|
|
||||||
|
// Keep screen on
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupFullscreen() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
window.setDecorFitsSystemWindows(false)
|
||||||
|
window.insetsController?.let {
|
||||||
|
it.hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
|
||||||
|
it.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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadSettings() {
|
||||||
|
// Load from DataStore
|
||||||
|
runBlocking {
|
||||||
|
dataStore.data.first().let { prefs ->
|
||||||
|
homepage = prefs[stringPreferencesKey("homepage")] ?: homepage
|
||||||
|
whitelist = (prefs[stringPreferencesKey("whitelist")] ?: "hobbs.farm")
|
||||||
|
.split(",").map { it.trim() }
|
||||||
|
idleTimeoutMs = (prefs[stringPreferencesKey("idle_timeout")]?.toLongOrNull()
|
||||||
|
?: 300) * 1000L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
private fun setupWebView() {
|
||||||
|
webView = binding.webView
|
||||||
|
|
||||||
|
webView.settings.apply {
|
||||||
|
javaScriptEnabled = true
|
||||||
|
domStorageEnabled = true
|
||||||
|
loadWithOverviewMode = true
|
||||||
|
useWideViewPort = true
|
||||||
|
setSupportZoom(true)
|
||||||
|
builtInZoomControls = true
|
||||||
|
displayZoomControls = false
|
||||||
|
}
|
||||||
|
|
||||||
|
webView.webViewClient = object : WebViewClient() {
|
||||||
|
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
|
||||||
|
val url = request.url.toString()
|
||||||
|
return if (isUrlAllowed(url)) {
|
||||||
|
resetIdleTimer()
|
||||||
|
false // Allow loading
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this@MainActivity, "URL blocked", Toast.LENGTH_SHORT).show()
|
||||||
|
true // Block and don't load
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
super.onPageFinished(view, url)
|
||||||
|
resetIdleTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
webView.webChromeClient = WebChromeClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isUrlAllowed(url: String): Boolean {
|
||||||
|
return whitelist.any { domain ->
|
||||||
|
url.contains(domain, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadHomepage() {
|
||||||
|
webView.loadUrl(homepage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearSession() {
|
||||||
|
webView.clearCache(true)
|
||||||
|
webView.clearHistory()
|
||||||
|
android.webkit.CookieManager.getInstance().removeAllCookies(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetIdleTimer() {
|
||||||
|
idleHandler.removeCallbacks(idleRunnable)
|
||||||
|
idleHandler.postDelayed(idleRunnable, idleTimeoutMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUserInteraction() {
|
||||||
|
super.onUserInteraction()
|
||||||
|
resetIdleTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
// Block back button - require admin PIN to exit
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
if (webView.canGoBack()) {
|
||||||
|
webView.goBack()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return super.onKeyDown(keyCode, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||||
|
super.onWindowFocusChanged(hasFocus)
|
||||||
|
if (hasFocus) {
|
||||||
|
setupFullscreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
resetIdleTimer()
|
||||||
|
setupFullscreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
idleHandler.removeCallbacks(idleRunnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secret admin gesture: triple-tap top-left corner to open settings
|
||||||
|
private var tapCount = 0
|
||||||
|
private val tapHandler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
fun onAdminAreaTap() {
|
||||||
|
tapCount++
|
||||||
|
tapHandler.removeCallbacksAndMessages(null)
|
||||||
|
tapHandler.postDelayed({ tapCount = 0 }, 500)
|
||||||
|
|
||||||
|
if (tapCount >= 3) {
|
||||||
|
tapCount = 0
|
||||||
|
startActivity(Intent(this, AdminActivity::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Kiosk Browser Settings"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="24dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Homepage URL"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etHomepage"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textUri" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Allowed Domains (comma-separated)"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etWhitelist"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Idle Timeout (seconds)"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etIdleTimeout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Admin PIN"
|
||||||
|
android:layout_marginBottom="24dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etAdminPin"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="numberPassword" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnSave"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="Save Settings" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnExit"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Back to Kiosk" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnExitKiosk"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:backgroundTint="#D32F2F"
|
||||||
|
android:text="Exit Kiosk Mode" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:id="@+id/webView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<!-- Invisible admin tap area (top-left corner) -->
|
||||||
|
<View
|
||||||
|
android:id="@+id/adminTapArea"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:layout_gravity="start|top"
|
||||||
|
android:onClick="onAdminAreaTap" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="primary">#1976D2</color>
|
||||||
|
<color name="primary_container">#BBDEFB</color>
|
||||||
|
<color name="white">#FFFFFF</color>
|
||||||
|
<color name="black">#000000</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Kiosk Browser</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.KioskBrowser" parent="Theme.Material3.DayNight.NoActionBar">
|
||||||
|
<item name="colorPrimary">@color/primary</item>
|
||||||
|
<item name="colorPrimaryContainer">@color/primary_container</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.KioskBrowser.Fullscreen" parent="Theme.KioskBrowser">
|
||||||
|
<item name="android:windowFullscreen">true</item>
|
||||||
|
<item name="android:windowContentOverlay">@null</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-policies>
|
||||||
|
<lock-task />
|
||||||
|
</uses-policies>
|
||||||
|
</device-admin>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Top-level build file
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application) apply false
|
||||||
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
[versions]
|
||||||
|
agp = "8.2.2"
|
||||||
|
kotlin = "1.9.22"
|
||||||
|
coreKtx = "1.12.0"
|
||||||
|
junit = "4.13.2"
|
||||||
|
junitVersion = "1.1.5"
|
||||||
|
espressoCore = "3.5.1"
|
||||||
|
appcompat = "1.6.1"
|
||||||
|
material = "1.11.0"
|
||||||
|
activity = "1.8.2"
|
||||||
|
constraintlayout = "2.1.4"
|
||||||
|
webkit = "1.10.0"
|
||||||
|
datastorePreferences = "1.0.0"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
|
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||||
|
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||||
|
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||||
|
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||||
|
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
||||||
|
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||||
|
androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref = "webkit" }
|
||||||
|
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePreferences" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "KioskBrowser"
|
||||||
|
include(":app")
|
||||||
Reference in New Issue
Block a user