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.

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

Next Post Previous Post

Add a comment