Привет! Меня зовут Максим Мищенко и я Android-разработчик в компании Effective.
В этой статье я расскажу, что такое Lock Task (или Kiosk) Mode и как его настроить.

Что такое Lock Task Mode (LTM)?

Lock Task Mode (или Kiosk Mode) — это совокупность методов Android API, позволяющих заблокировать устройство в одном приложении. 

Пользователь не может выйти из приложения, получить push от других приложений, а также выключить устройство.

LTM используется в ситуациях, когда:

  1. Устройством пользуется большой поток пользователей, и нам нужно ограничить их возможности;

  2. Компания выдает сотруднику устройство для работы и хочет, чтобы он был сосредоточен на рабочих задачах и не отвлекался на сторонние приложения.

Реализовываем LTM

Настраиваем DeviceAdminReceiver

Создаем класс наследник и прописываем его в манифесте:

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>

В device_admin_receiver.xml перечисляем нужные нам права. По сути AdminReciver — это обычный BroadcastReciver, который перехватывает системные сообщения, связанные со сменой пароля, включение LTM и прочим, что мы описываем в 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>

Далее пользователь должен выдать разрешение администратора:

 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

Выдаем права owner нашему приложению, используя adb:

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

Device Owner App — приложение, которое является владельцем устройства. Чтобы войти в режим Owner, выходим из всех аккаунтов (Google, Xiaomi, Samsung). Важно отметить, что по итогу доступ в Google Play закрыт.

Запускаем activity в LTM

  1. Прежде всего разрешаем activity перейти в LTM с помощью поля 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. Используем ActivityManager, чтобы убедиться, что мы еще не в LTM

  2. C помощью DevicePolicyManager проверяем наличие нужных прав

  3. Добавляем приложение в белый список. Все приложения, не находящиеся в нём, будут принудительно закрыты

Важно: если приложению необходимы сторонние процессы, их также следует добавить в белый список

  1. Снова запускаем activity в зависимости от версии Android

    В новых версиях можно использовать options.setLockTaskEnabled(true) для включения LTM, а в Android 9 и ниже необходимо вызвать 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()
        }
    }

Выключаем LTM

Способ 1:

Для снятия LTM можно отобрать права owner с помощью adb:

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

Способ 2:

Для выключения LTM можно вызвать dpm.clearDeviceOwnerApp(packageName). У приложения будут отозваны права owner, и оно выйдет из LTM.

Способ 3:

Если activity было запущено при помощи startLockTask() то и остановлено оно может быть при помощи stopLockTask() В остальных случаях достаточно просто убрать приложение из списка разрешенных и система автоматически закроет его:

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

Настраиваем системный интерфейс

При переходе в LTM привычный интерфейс системы выключается, остаётся только кнопка назад. Начиная с Android 9 можно настраивать системный интерфейс, устанавливая LOCK_TASK_FEATURE:

dpm.setLockTaskFeatures(adminName, DevicePolicyManager.LOCK_TASK_FEATURE_GLOBAL_ACTIONS)

UI-фича

Описание

LOCK_TASK_FEATURE_HOME

Активна кнопка Home. Если используется пользовательский лаунчер, нажатие на кнопку Home не приводит к каким-либо действиям, до того момента, пока мы не внесем в белый список стандартный лаунчер Android.

LOCK_TASK_FEATURE_OVERVIEW

Активна кнопка Overview, нажатие на нее открывает экран Recents. Если мы включаем эту кнопку, мы должны также включить кнопку Home.

LOCK_TASK_FEATURE_GLOBAL_ACTIONS

Включает диалог global actions, который отображается при длительном нажатии на кнопку питания. Единственная фича, которая включается, когда не была вызвана setLockTaskFeatures(). Обычно пользователь не может выключить устройство, если мы отключим этот диалог.

LOCK_TASK_FEATURE_NOTIFICATIONS

Включает уведомления для всех приложений. При этом отображаются иконки приложений в статус-баре, всплывающие уведомления и расширяемая системная шторка. Если мы включаем эту кнопку, мы должны также включить кнопку Home. При этом панель Quick Settings остается неактивной. 

LOCK_TASK_FEATURE_SYSTEM_INFO

Активна часть статус-бара, в которой содержится системная информация: состояние связи, заряд батареи, звуковые настройки

LOCK_TASK_FEATURE_KEYGUARD

Активен любой локскрин, который может быть установлен на устройстве. Обычно не подходит для устройств с публичным доступом (например, стойки информации или цифровые подписи)

LOCK_TASK_FEATURE_NONE

Отключает все system UI features, описанные выше

Дополнительные меры по ограничению пользователя

Главная цель LTM — не дать выйти из приложения, однако привилегированный пользователь должен уметь это сделать.

Автостарт

LOCK_TASK_FEATURE_GLOBAL_ACTIONS даёт пользователю перезагрузить устройство, тем самым выйти из LTM. Чтобы предотвратить такое поведение, необходимо настроить автостарт приложения.

Когда система загружается, она посылает широковещательное сообщение BOOT_COMPLETED. Поэтому достаточно настроить broadcast receiver на это сообщение и из ресивера запустить activity:

        <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)
    }
}

Не всякое приложение можно запустить с background. Наше приложение — Device Owner, и ему это позволено.

Выход по паролю

Для предотвращения несанкционированного выхода из LTM можно воспользоваться паролем. Вариант решения:

  1. При первом запуске генерируем UUID и сохраняем его в SharedPreferences

  2. При последующих запусках достаём UUID из SharedPreferences

  3. При старте отправляем запрос с UUID и дополнительной информацией для регистрации устройства в базе данных

  4. При попытке выйти отправляем запрос с UUID для генерации пароля на сервер

  5. Пользователь вводит пароль

  6. Мы отправляем пароль с UUID на сервер, сервер отвечает, подошёл пароль или нет

В данном случае я использовал M2M (Machine-to-Machine) авторизацию — по сути обычную Auth0, только мы не вводим пароль, а сразу размениваем секрет на access token. Использовал я ее потому, что возможен случай выхода из LTM ещё неавторизованного пользователя. Если в приложение возможен выход только после авторизации, то можно использовать access token, полученный после авторизации. Помимо этого могут быть настроены другие способы аутентификации (например, apiKey). 

Мой проект на GitHub: github.com/UserNameMax/kiosk-demo

Комментарии (0)