SJ cartoon avatar

Mobile Android Kiosk Mode Without Root

Articles written as recently as this month are still telling me that to write an app to show Android Kiosk mode in action, I -must- a) buy a commercial app, b) hack code together to override the home button, or c) have root access. As I wrote a sample app this evening without any of the above, I can say that needing a, b, or c is total fear-mongering malarkey. Malarkey I say!

So, full disclosure… I just really wanted to say malarkey. But what I said is true - you don’t need any fancy pants hacks and slashes to write a kiosk app, you just need Lollipop! And it wouldn’t hurt to have my sample repo to make life a bit easier: https://github.com/sureshjoshi/android-kiosk-example

These ain’t your grandfather’s kiosks

A great example of an Android kiosk in action is in my feature picture. The team over at teaBOT created a machine which allows tea drinkers to select and create custom loose leaf tea blends and have them served up piping hot in under 30 seconds. Having tried it out a lot, I can say that it’s pretty awesome and the tea is delicious - peppermint tease FTW!

Similarly, the project I need to create a kiosk app for is to have people interact with a sophisticated piece of hardware - in a way anyone could understand and nobody could screw up. This means immediate feedback of equipment usage, alongside graphics of how to use the product and pretty, animated charts with the results.

Remedial screen pinning and task locking

Digressions aside, most of this project comes down to using Android API level 22’s 21’s screen pinning functionality (Settings -> Security -> Screen Pinning).

Screen pinning allows you to select an app to pseudo-lock the screen to. From the app overview screen, hit the pin button on the bottom-right of the app you’re interested in, and you’ll get asked for a confirmation if you want to pin the screen.

Once pinned, the status bar (top of the screen) disappears, and the navigation buttons (bottom of screen) are individually useless. Depending on how you’ve set your Screen Pinning settings, you also won’t receive any notifications from your device.

To exit pinned mode, press the ‘back’ and ‘overview’ buttons together, and you’ll get kicked to the lock screen. Once you unlock your device, your app will no longer be pinned.

Two kickers:

  • You have to get the user’s permission to enter pinned mode (not a big deal for kiosks, just weird)
  • Anyone can bounce the app to the lock screen, rendering the kiosk useless

Advanced remedial screen pinning

The workaround for both of these problems is to set a ‘device owner’, and allow that device owner to transparently lock the device.

There are three ways to set a device owner:

I prefer the last route, because it takes about 10 seconds and my ADB is always a few keystrokes away anyways.

ADBs, DPMs, and other acronyms

The one annoying prerequisite to setting a device owner is that the device needs to be ‘unprovisioned’. For me, this meant removing my associated Google account, provisioning the device, then re-adding my account. This may seem strange, but for external kiosks or enterprise users - this is a no-brainer, as those wouldn’t typically be hooked up to a Google account anyways.

For the DPM route to work, your app with its corresponding app ID needs to be installed on the device, and the app needs to have a ‘DeviceAdminReceiver’ setup.

Setting up the DeviceAdminReceiver is pretty simple. First, add a receiver to your AndroidManifest as below:

<receiver
    android:name="com.sureshjoshi.android.kioskexample.AdminReceiver"
    android:label="@string/app_name"
    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>

Next, create the class that implements the DeviceAdminReceiver:

public class AdminReceiver extends DeviceAdminReceiver {
}

The class doesn’t have to do anything, but for the sake of seeing what was going on, I put toasts in each of the methods which indicated my permissions were changing modes - check it out here.

Once you have this app on your device, go to the command line and run:

adb shell dpm set-device-owner com.sureshjoshi.android.kioskexample/.AdminReceiver

Which should give you something like:

Success: Device owner set to package com.sureshjoshi.android.kioskexample
Active admin set to component {com.sureshjoshi.android.kioskexample/com.sureshjoshi.android.kioskexample.AdminReceiver}

From here you’re home free… The API calls to setLockTaskPackages, startLockTask, and stopLockTask are all you need to enter and exit kiosk mode (setLockTaskPackages allows you to pin the screen without asking for user confirmation - but only works when you’re the device owner for the app).

Here is my onCreate:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    ButterKnife.bind(this);

    ComponentName deviceAdmin = new ComponentName(this, AdminReceiver.class);
    mDpm = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);
    if (!mDpm.isAdminActive(deviceAdmin)) {
        Toast.makeText(this, getString(R.string.not_device_admin), Toast.LENGTH_SHORT).show();
    }

    if (mDpm.isDeviceOwnerApp(getPackageName())) {
        mDpm.setLockTaskPackages(deviceAdmin, new String[]{getPackageName()});
    } else {
        Toast.makeText(this, getString(R.string.not_device_owner), Toast.LENGTH_SHORT).show();
    }

    mDecorView = getWindow().getDecorView();

    mWebView.loadUrl("https://www.vicarasolutions.com/");
}

And here is where I enable and disable Android kiosk mode:

private void enableKioskMode(boolean enabled) {
    try {
        if (enabled) {
            if (mDpm.isLockTaskPermitted(this.getPackageName())) {
                startLockTask();
                mIsKioskEnabled = true;
                mButton.setText(getString(R.string.exit_kiosk_mode));
            } else {
                Toast.makeText(this, getString(R.string.kiosk_not_permitted), Toast.LENGTH_SHORT).show();
            }
        } else {
            stopLockTask();
            mIsKioskEnabled = false;
            mButton.setText(getString(R.string.enter_kiosk_mode));
        }
    } catch (Exception e) {
        // TODO: Log and handle appropriately
    }
}

To add a little bit of UI cleanliness to the mix, here is the code that puts you into immersive mode, so that the top and bottom bars are hidden by default, and show up when the top/bottom of the screen is dragged inwards (this code does it without resizing):

@Override
protected void onResume() {
    super.onResume();
    hideSystemUI();
}

// This snippet hides the system bars.
private void hideSystemUI() {
    // Set the IMMERSIVE flag.
    // Set the content to appear under the system bars so that the content
    // doesn't resize when the system bars hide and show.
    mDecorView.setSystemUiVisibility(
            View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
                    | View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar
                    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}

Wrapping up

To put it all together, the app I wrote simply shows a webview pointed at https://www.vicarasolutions.com, with a button allowing you to enable or disable kiosk mode. The app requires device owner privileges as previously mentioned and otherwise works in immersive mode.

Here is what you should see when not in kiosk mode:

And in kiosk mode:

Security concerns

Note that the back button just pops up a toast telling you that you don’t have permission to close the app. Here you could write some back button code to pop up a password screen - so that users with the correct password can actually close the app.

If you do the above, and you’re distributing your APK, please understand that if you hardcode your password in the APK, it will take about 10 seconds to hack your app… Maybe 20 seconds. I think securing passwords and keys in Android apps would be a great post in the future, because I didn’t realize how many companies still don’t realize that any strings in APKs are transparent to whomever has the APK (in Java in general… Actually, arguably in any language to some extent).

Also, the tablet can always be restarted by anyone with physical access to the power button - so, for kiosks, I always recommend designing an enclosure where the USB port and power/volume buttons are not easily accessible. Also, for good measure, you may want to hook into the *_BOOT_COMPLETED actions on Android, so that you can immediately start your app when the tablet boots up. Here’s an SO post about it.

Update: By the way…

Charly in the comments mentioned about deleting the app once you’ve enabled it as a Device Administrator. This is a little roundabout, because the idea behind the device administrator is that it’s not for Google Play store apps, but rather for enterprises where an IT administrator can set these up - and they’re intended for enterprise use afterwards.

Referring to Florent’s blog (also, Florent has a few more posts about pinning apps here and here):

Notice that once the Device Owner application is set, it cannot be unset with the dpm command. You’ll need to programmatically use the DevicePolicyManager.clearDeviceOwnerApp() method or factory reset your device.

In my example above, I’ve added the following to the source code somewhere:

mDpm.clearDeviceOwnerApp("com.sureshjoshi.android.kioskexample");

Try to delete the app, and if you still run into trouble, go to Settings -> Security -> Device Administrators and uncheck the Kiosk app as a device administrator, then re-try uninstalling.

Update: Updating…

I’ve written another post about updating Your Android kiosk app. It covers three different methods of performing an over-the-air kiosk app update with varying levels of effort.

That’s about it for me. If you’ve got any suggestions for topics I should cover, feel free to leave a comment or tweet me [@SJoshi84](https://twitter.com/SJoshi84).