Setting up Lock Task Mode aka Kiosk Mode

Hello! My name is Maxim Mishchenko and I am an Android developer at Effective.
In this article I will tell you what Lock Task (or Kiosk) Mode is and how to set it up.

What is Lock Task Mode (LTM)?

Lock Task Mode (or Kiosk Mode) is a collection of Android API methods that allow you to lock a device in a single application.

The user cannot exit the application, receive push from other applications, or turn off the device.

LTM is used in situations where:

  1. The device is used by a large number of users, and we need to limit their capabilities;

  2. The company gives the employee a device for work and wants him to be focused on work tasks and not be distracted by third-party applications.

We implement LTM

Setting up DeviceAdminReceiver

We create a successor class and write it in the manifest:

class AdminReceiver : DeviceAdminReceiver() {
    private val TAG = "AdminReceiver"
    companion object {
        fun getComponentName(context: Context): ComponentName {
            return ComponentName(context.applicationContext, AdminReceiver::class.java)
        }
    }
    
}
<receiver
            android:name=".AdminReceiver"
            android:exported="true"
            android:permission="android.permission.BIND_DEVICE_ADMIN">
            <meta-data
                android:name="android.app.device_admin"
                android:resource="@xml/device_admin_receiver" />
            <intent-filter>
                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
                <action android:name="android.intent.action.PROFILE_PROVISIONING_COMPLETE" />
            </intent-filter>
        </receiver>

In device_admin_receiver.xml we list the rights we need. Essentially, AdminReciver is a regular BroadcastReciver that intercepts system messages related to changing the password, enabling LTM, and other things that we describe in device_admin_receiver.xml:

<device-admin>
    <uses-policies>
        <limit-password />
        <watch-login />
        <reset-password />
        <force-lock />
        <wipe-data />
        <expire-password />
        <encrypted-storage />
        <disable-camera />
        <disable-keyguard-features />
        <reset-password />
        <wipe-data />
        <expire-password />
        <set-global-proxy />
    </uses-policies>
</device-admin>

Next, the user must grant administrator permission:

 val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN).apply {
            putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, adminName)
            putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION, "")
        }
  startActivityForResult(intent, 1)

Device Owner

We grant owner rights to our application using adb:

adb shell dpm set-device-owner [package]/.AdminReceiver

Device Owner App is an application that owns the device. To enter Owner mode, log out of all accounts (Google, Xiaomi, Samsung). It is important to note that as a result, access to Google Play is closed.

Launching activity in LTM

  1. First of all, we allow the activity to go to LTM using the field android:lockTaskMode

    		<activity
            android:name=".MainActivity"
            android:exported="true"
            android:lockTaskMode="if_whitelisted">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
  1. Using ActivityManager to make sure we are not in LTM yet

  2. Using DevicePolicyManager, we check the presence of the necessary rights

  3. Add the application to the white list. All applications not located in it will be forcibly closed

Important: if the application requires third-party processes, they should also be whitelisted

  1. We launch the activity again depending on the Android version

    In new versions you can use options.setLockTaskEnabled(true) to enable LTM, and in Android 9 and below you need to call startLockTask()

private fun runKiosk() {
        val context = this

        val dpm = getSystemService(DEVICE_POLICY_SERVICE) as DevicePolicyManager
        val adminName = AdminReceiver.getComponentName(context)
        val KIOSK_PACKAGE = "com.example"
        val APP_PACKAGES = arrayOf(KIOSK_PACKAGE)
        val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 

				val isKiosk = activityManager.lockTaskModeState == ActivityManager.LOCK_TASK_MODE_LOCKED
				val isNotHavePermissions = !dpm.isDeviceOwnerApp(adminName.packageName)
				
        if (isKiosk || isNotHavePermissions) return

        val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN).apply {
            putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, adminName)
            putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION, "")
        }
        startActivityForResult(intent, 1)

        dpm.setLockTaskPackages(adminName, APP_PACKAGES)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            // Set an option to turn on lock task mode when starting the activity.
            val options = ActivityOptions.makeBasic()
            options.setLockTaskEnabled(true)

            // Start our kiosk app's main activity with our lock task mode option.
            val packageManager = context.packageManager
            val launchIntent = packageManager.getLaunchIntentForPackage(KIOSK_PACKAGE)
            if (launchIntent != null) {
                context.startActivity(launchIntent, options.toBundle())
            }
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        runKiosk()
    }
    
    override fun onResume() {
        super.onResume()
        val dpm = getSystemService(DEVICE_POLICY_SERVICE) as DevicePolicyManager
        if (dpm.isLockTaskPermitted(packageName) && Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
            startLockTask()
        }
    }

Turn off LTM

Method 1:

To remove LTM, you can take away owner rights using adb:

adb shell dpm remove-active-admin [package]/.AdminReceiver

Method 2:

To disable LTM, you can call dpm.clearDeviceOwnerApp(packageName). The application's owner rights will be revoked and it will exit LTM.

Method 3:

If the activity was started using startLockTask() then it can be stopped with the help stopLockTask() In other cases, you just need to remove the application from the list of allowed ones and the system will automatically close it:

fun disableKiosk() {
        val dpm = getSystemService(DEVICE_POLICY_SERVICE) as DevicePolicyManager
        val adminName = AdminReceiver.getComponentName(this)
        dpm.setLockTaskPackages(adminName, emptyArray())
    }

Setting up the system interface

When switching to LTM, the usual system interface is turned off, leaving only the back button. Starting with Android 9, you can customize the system interface by setting LOCK_TASK_FEATURE:

dpm.setLockTaskFeatures(adminName, DevicePolicyManager.LOCK_TASK_FEATURE_GLOBAL_ACTIONS)

UI feature

Description

LOCK_TASK_FEATURE_HOME

The Home button is active. If you're using a custom launcher, pressing the Home button won't do anything unless you whitelist the stock Android launcher.

LOCK_TASK_FEATURE_OVERVIEW

The Overview button is active; clicking on it opens the Recents screen. If we enable this button, we must also enable the Home button.

LOCK_TASK_FEATURE_GLOBAL_ACTIONS

Enables the global actions dialog, which is displayed when you long press the power button. The only feature that is enabled when setLockTaskFeatures() was not called. Usually the user cannot turn off the device if we disable this dialog.

LOCK_TASK_FEATURE_NOTIFICATIONS

Enables notifications for all applications. At the same time, application icons are displayed in the status bar, pop-up notifications and an expandable system curtain. If we enable this button, we must also enable the Home button. In this case, the Quick Settings panel remains inactive.

LOCK_TASK_FEATURE_SYSTEM_INFO

The part of the status bar is active, which contains system information: communication status, battery charge, sound settings

LOCK_TASK_FEATURE_KEYGUARD

Any lockscreen that can be installed on the device is active. Typically not suitable for publicly accessible devices (such as information racks or digital signatures)

LOCK_TASK_FEATURE_NONE

Disables all system UI features described above

Additional user restriction measures

The main goal of LTM is to prevent the user from logging out of the application, but the privileged user must be able to do this.

Autostart

LOCK_TASK_FEATURE_GLOBAL_ACTIONS allows the user to reboot the device, thereby exiting LTM. To prevent this behavior, you need to configure the application to autostart.

When the system boots, it sends a BOOT_COMPLETED broadcast message. Therefore, it is enough to configure the broadcast receiver for this message and launch the activity from the receiver:

        <receiver
            android:name=".StartBroadcastReceiver"
            android:exported="true"
            android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </receiver>
class StartBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val packageManager = context.packageManager
        val launchIntent = packageManager.getLaunchIntentForPackage("com.example")
        context.startActivity(launchIntent)
    }
}

Not every application can be launched with a background. Our application is the Device Owner and is allowed to do so.

Exit by password

To prevent unauthorized exit from LTM, you can use a password. Solution option:

  1. At the first launch, we generate a UUID and save it in SharedPreferences

  2. On subsequent launches, we retrieve the UUID from SharedPreferences

  3. At startup, we send a request with UUID and additional information to register the device in the database

  4. When trying to log out, we send a request with UUID to generate a password to the server

  5. User enters password

  6. We send the password from the UUID to the server, the server responds whether the password is correct or not

In this case, I used M2M (Machine-to-Machine) authorization – essentially ordinary Auth0, only we do not enter a password, but immediately exchange the secret for an access token. I used it because it is possible for an unauthorized user to exit LTM. If the application can only be accessed after authorization, then you can use the access token received after authorization. In addition, other authentication methods can be configured (for example, apiKey).

My project on GitHub: github.com/UserNameMax/kiosk-demo

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *