A kiosk app is usually an app that forces an Android device to be used for a single specific purpose, preventing the user from exiting it or from using device features outside of the app itself.
In this post I would like to show how to create a kiosk app that works as a browser locked to full screen without the possibility for user to exit the app.
The app should be able to load preconfigured URLs and possibly also allow to blacklist/whitelist URLs, so that user cannot accidentally navigate out of the web app by clicking a link on a web page.
In the first part of this post I'll show how to use the device owner mode to lock the app to the screen. I wrote more about device owner mode in my previous blogposts, so you might want to check it out if you need more information.
The second part of this blog post should show you how to use Android's WebView to load a specific URL. You might not have complete control about what links are shown on web page that you load. Therefore, I'll also try to describe how to block specific links, so that the user cannot navigate away from your kiosk web app.
This post contains code from my kiosk browser app published on Google Play. If you're only interested in a quick solution that makes your web app a kiosk app, you should check it out. It automates the installation and device owner activation - you can just plug in your target device via USB OTG and configure the kiosk browser through a simple configuration interface.
You can find sample code related to this blog post on github.
If you you're an Android enthusiast that likes to learn more about Android internals, I highly recommend to check out my Bugjaeger app. It allows you to connect 2 Android devices through USB OTG and perform many of the tasks that are normally only accessible from a developer machine via ADB directly from your Android phone/tablet.
Making Your App Device Owner
Device owner app can add a package (including its own package) to the list of packages that are allowed to go into lock task mode.
I showed how to create a simple device owner app in my previous post. Here I'll try to quickly summarize the steps.
You can start by implementing a DeviceAdminReceiver
class DevAdminReceiver: DeviceAdminReceiver() {
override fun onEnabled(context: Context?, intent: Intent?) {
super.onEnabled(context, intent)
Log.d(TAG, "Device Owner Enabled")
}
}
Create device_admin.xml
resource file inside of res/xml
with metadata that contains information related to device administration
<?xml version="1.0" encoding="utf-8"?>
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
<uses-policies>
</uses-policies>
</device-admin>
Declare your DevAdminReceiver
component inside of AndroidManifest.xml
<application
android:testOnly="true"
...>
...
<receiver
android:name="eu.sisik.kioskbrowser.DevAdminReceiver"
android:label="@string/app_name"
android:permission="android.permission.BIND_DEVICE_ADMIN" >
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_admin" />
</receiver>
...
</application>
Please note that I also added the testOnly flag to the <application>
tag. If you are enabling device owner via ADB, this flag is necessary since Android 7.0.
Once you're app is complete and installed on the device, you can activate it as device owner via ADB by executing the following shell command
adb shell dpm set-device-owner eu.sisik.kioskbrowser/.DevAdminReceiver
Pinning App to The Screen
As mentioned earlier, for an app to be allowed to go into lock task mode, it needs to be added to a special list of packages that can do so. You can use the method setLockTaskPackages() to achieve this
val dpm = context?.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager?
val cn = ComponentName(context, DeviceAdminReceiver::class.java!!)
dpm?.setLockTaskPackages(cn, arrayOf(context?.packageName))
You may put this piece of code into onEnabled()
method of DeviceAdminReceiver
that I showed in previous section. There you can be sure that you already are enabled as device owner.
It is also possible to call it directly from your Activity or Fragment, but you should first check if you are a device owner before calling it
val dpm = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
val cn = ComponentName(this, DevAdminReceiver::class.java!!)
if (dpm.isDeviceOwnerApp(packageName)) {
// We are a device owner - lets allow our package to use lock task
val packages = arrayOf(packageName)
dpm.setLockTaskPackages(cn, packages)
} else {
// Not yet enabled as device owner
// ...
}
Now that you made sure that you're app is a device owner and it is added to the list of packages that can use lock task, you can actually call startLockTask() and pin your app to the screen
val dpm = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
if (dpm.isLockTaskPermitted(packageName)) {
startLockTask()
} else {
Toast.makeText(this, getString(R.string.err_locktask_not_permitted), Toast.LENGTH_LONG).show()
finish()
}
A suitable time to call this method might be inside of your Activity's onResume()
method. The method should only be called when activity is in foreground.
When I experimented with lock task, I randomly ran into issue when startLockTask()
failed even when activity should already be in foreground. There is stackoverflow post reporting similar issue, so other people might also have problems with this. In case you also run into issues when calling startLockTask()
, you might try to delay the call slightly using a Handler()
.
Using WebView to Load Specific Web Page
Android's [WebView]() is a very powerful UI component that provides all the basic functionality of a browser. It can load HTML/CSS and execute JavaScript and is often used in hybrid frameworks like Ionic or Apache Cordova.
Before loading web pages in a WebView, you again need to update your AndroidManifest.xml
file, so that it contains additional permission for accessing internet
<manifest ...>
<uses-permission android:name="android.permission.INTERNET"/>
...
If your app is completely web-based, there usually don't need to be that many UI components, so the layout file loaded with setContentView()
inside of your Activity's onCreate()
callback can be very simple
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
To load a specific URL, you only need to call
webView.loadUrl("https://sisik.eu")
Implementing URL Whitelist/Blacklist
There can be situations where you don't have complete control over what clickable links get displayed in your web app. Or you want to still give user access to browse on the internet, but only restrict specific links.
In those cases you can implement a whitelisting or blacklisting mechanism using a WebViewClient
class KioskWebViewClient : WebViewClient() {
// Whitelisted hosts
private val allowedHosts = arrayOf(
"sisik.eu",
"www.sisik.eu"
)
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
// Allow to load only whitelisted hosts
if (allowedHosts.contains(request?.url?.host))
return false
// Not allowed to load this url - do nothing..
return true
}
}
The code implements host whitelisting, where the KioskWebViewClient
only allows to load links from my blog.
You can turn this around to implement a blacklist, although with blacklists it can be more difficult to find the problematic hosts when there exists so many possibilities.
This mechanism can be very flexible - you can implement filters based on various regular expressions
and use other properties of request besides the host.
To configure the WebView
, so that is uses our KioskWebViewClient
you only need to do the following
webView.webViewClient = KioskWebViewClient()
// Now you can load your URL
webView.loadUrl("https://sisik.eu")
Starting Automatically After Reboot
Your kiosk device will occasionally need to be rebooted and you probably don't want to start your kiosk app manually. Therefore, one thing you can do is to listen for the BOOT_COMPLETED broadcast and start your app automatically after reboot.
To restart the app after reboot, first update your AndroidManifest.xml
...
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<application
...>
...
<receiver android:name="eu.sisik.kioskbrowser.BootCompletedReceiver">
<intent-filter >
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
...
Make sure you don't forget to add the RECEIVE_BOOT_COMPLETED permisson, otherwise your BroadcastReceiver
that is listening for BOOT_COMPLETED
won't start.
Android system will now automatically notify your static BroadcastReceiver when the system has completed rebooting. You can then start the Activity
that is showing your WebView
like this
class BootCompletedReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val i = Intent(context, MainActivity::class.java)
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(i)
}
}
Make it a Launcher
Another thing you can try is to configure your main kiosk Activity to start as a launcher. To do this, you again need to change your AndroidManifest.xml
<application
...>
...
<activity android:name=".MainActivity">
<intent-filter>
<!-- Make this activity behave like a launcher -->
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
In the code above I've added CATEGORY_DEFAULT, so that MainActivity
can be presented as an option when user presses Home
button. Additionally, I've added CATEGORY_HOME which should allow this activity to be displayed as the first thing after reboot.
Block Other Apps
Another thing you can do to restrict the use of your kiosk device only to your specific kiosk app is to block other apps
val dpm = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
dpm.setApplicationHidden(componentName, "some.package.name", false)
This will hide the package from launcher and make it inaccessible to users. Some packages might be actually useful or required to make your device function properly, so don't just blindly disable everything. Do some research first.
Useful Links
I've written several other blogposts related to device owner. If you need more information about this topic, I recommend you check them out
sisik.eu/blog:device owner
If you already have your web app, and you only want a quick solution that makes your app run as a kiosk browser on an Android device, you can check out my app on Google Play.
It contains a configurable kiosk browser and automates all the activation steps, so you don't even need any developer tools or ADB installed to make it work. You just connect your Android phone to your target Android kiosk device via USB OTG, and activate the browser with one tap. The configuration interface contains ads, but the actual installed browser is, of course, ad-free.
play.google.com/store/apps/details?id=eu.sisik.kioskconfigurator
If you're trying to enable device owner, install APK files, or do some kind of maintenance of your kiosk when the device is already deployed, you might find my Bugjaeger app useful.
It contains ADB embedded into regular Android app. So you can do a lot of things that you would normally do from development machine, directly by connecting 2 Android devices through USB OTG cable
play.google.com/store/apps/details?id=eu.sisik.hackendebug