Android 5.0 kiosk mode (aka "super-immersive" aka "single-app-device" aka "screen lock" aka "task lock") tutorial


Introduction

Once upon a time I've got two similar Android projects: both of them were an apps for embedded "single-app-use" devices. First project was an app for pizza ordering, placed on a tablet in a hotel, second was a ticket machine for a cinema. Common challenge for both apps is that end-user should not have access to android itself while our app is running.

How can I hide system controls and prevent user from leaving my app? Beginning from Android 5.0 Lollipop (API level 21 and above) there is a built-in API for that. There is no 100% working no-root solution for devices <21.

Unfortunately, I could find only small pieces of information about this API in official docs and stackoverflow. Here I will put all my experience about in one place

Immersive mode

Beginning from Android KitKat 4.4 your app can be runned in "Immersive" mode. Immersive mode is used generally for games and movie players, allowing you to hide system bar and navigation bar (where "Back", "Home" and "Recent" buttons are).


(animated gif, click to view)

In particular, we are interested in "Immersive-sticky" mode, it is described well in official documentation. Just override onWindowFocusChanged() method in your activity in the following way:
MainActivity.java
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
            getWindow().getDecorView().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
                | View.SYSTEM_UI_FLAG_FULLSCREEN
                | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
        }
    }
This is pretty nice for beginning - system controls are hidden. But that's not enougth, as far as "swipe down" or "swipe up" gesture makes system controls visible back, so user can freely go to Home Screen and make a factory reset (= You cannot disable this swipe.

Lock Task (Pinned mode)

Beginning from Android Lollipop 5.0 there is an ability to lock (pin) your activity on the screen. This can be done by calling Activity.startLockTask() method. Activity lock hides all the information from status bar and blocks "Back", "Home" and "Recent" buttons. Activity stays on foreground until Activity.stopLockTask() or Activity.finish() method is called. This is how we do it:
MainActivity.java
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate (Bundle savedInstanceState) {
        super.onCreate (savedInstanceState);
        setContentView (R.layout.activity_main);
  
        //Optional
        findViewById (R.id.some_button).setOnClickListener (new View.OnClickListener () {
            @Override
            public void onClick (View view) {
                stopLockTask ();
            }
        });
    }


    @Override
    protected void onResume () {
        super.onResume ();
        startLockTask();
    }
}
Note 1. I personally do not recommend use Activity.finish() call without Activity.stopLockTask() call - this leads to unstable behaviour sometimes
Note 2. Sometimes I've faced that calling Activity.stopLockTask() when Activity is not actually in locked mode causes crash. So you'd better check if Activity is really locked before calling Activity.stopLockTask(). See full demo below.

Both notes are not tested well, but take it into condition in case of some problems

Lock task can be used in conjunction with immersive mode or without one. In first case system controls will became visible with swipe, in second will stay on the screen all time, but in both cases buttons will be locked.


(animated gif, click to view)

This is 80% what we need... wht just 80%? Just because user is still able to leave the locked mode by touching "Back" and "Recent" button together (or long press on Back for emulator). Moreover, Android will kindly suggest it to user with a "Built-in" toast messages.

Indeed, this behaviour is quite reasonable. No one user wants his "fresh-downloaded-from-google-play" app to block the whole device, right? But there is an exception if you are developer and developing for you own device, as usually happens with embedded devices...

Device administrator (owner) app

If you have a developer access to device, you can grant a device administrator right to an app. Application having administrator rights can absoleutly lock the task.

Here is how you can setup an app to be an administrator

1) Add a DeviceAdminReceiver to your app. DeviceAdminReceiver is a broadcast receiver which receives some system administration events. You're not needed to handle these events for this task, but you just should define a receiver properly.

Just create a class:

MyDeviceAdminReceiver.java
import android.app.admin.DeviceAdminReceiver;

public class MyDeviceAdminReceiver extends DeviceAdminReceiver {
}
then add manifest declaration
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest package="ru.pvolan.testkiosk"
          xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        ...>
        
        ...

        <receiver android:name=".MyDeviceAdminReceiver"
                  android:description="@string/admin_receiver_description"
                  android:label="@string/admin_receiver_label"
                  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>
Ohhh, yes, you will also need some resources
res/values/strings.xml
<resources>
    <string name="app_name">TestKiosk</string>
  
    <string name="admin_receiver_label">Tesk Kiosk label</string>
    <string name="admin_receiver_description">Tesk Kiosk description</string>
</resources>
res/xml/device_admin.xml
<?xml version="1.0" encoding="utf-8"?>
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-policies>
    </uses-policies>
</device-admin>

2) Modify activity code a little. Instead of simple "startLockTask()" we need some more code:

MainActivity.java
@Override 
protected void onResume () {
    super.onResume ();    
    requestTaskLock();
}

private void requestTaskLock() {
    DevicePolicyManager dpm = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);
    ComponentName deviceAdminReceiver = new ComponentName(this, MyDeviceAdminReceiver.class);
    if (dpm.isDeviceOwnerApp(this.getPackageName()))
    {
        String[] packages = {this.getPackageName()};
        dpm.setLockTaskPackages(deviceAdminReceiver, packages);
        if (dpm.isLockTaskPermitted(this.getPackageName())) {
            startLockTask();
        } else {
            Toast.makeText (this, "Lock screen is not permitted", Toast.LENGTH_SHORT).show ();
        }
    } else { 
        Toast.makeText (this, "App is not a device administrator", Toast.LENGTH_SHORT).show ();
    }
}

Code for exiting lock task mode is just the same.

3) Setup your app as a device administrator. To do this:
  1. Do factory reset on your device, or create a new Android emulator
  2. Turn device on. IMPORTANT! Do NOT create Google account. If you create google account, this account becames a device administrator. You'll unable to set another administrator before you make another factory reset
  3. Install your app onto device
  4. Plug device to your PC in "developer mode" and run
    adb shell dpm set-device-owner [your.app.id]/[full.name.of.your.admin.receiver.class]
    Example for my sample app is
    adb shell dpm set-device-owner ru.pvolan.testkiosk/ru.pvolan.testkiosk.MyDeviceAdminReceiver
    You should get output like this:
    Success: Device owner set to package ComponentInfo{ru.pvolan.testkiosk/ru.pvolan.testkiosk.MyDeviceAdminReceiver}
    Active admin set to component {ru.pvolan.testkiosk/ru.pvolan.testkiosk.MyDeviceAdminReceiver}
    
  5. Check device's "Settings - Security - Device Administrators" menu. Now you should see your app in the list.

4) Have profit!


(animated gif, click to view)

If you did everything right. now you should be able to run your app in a full lock mode. This means user is unable to leave your activity until you call stopLockTask() method. "Home", "Recent" buttons and status bar are invisible (their hardware analogs are blocked). "Back" button stays visible via swipe, but is deactivated (it auto-activates on some events such as cancellable alert or keyboard opened).

Usually it is useful to exit lock mode by triggering some secret conditions, such as password entering, for example.

Here is a full demo https://github.com/PVoLan/android-kiosk

Hope this publication will increase a quantity of android-based kiosk-machines and other embedded devices.

(=

1 комментарий:

  1. Этот комментарий был удален администратором блога.

    ОтветитьУдалить