Привет! Меня зовут Максим Мищенко и я Android-разработчик в компании Effective.
В этой статье я расскажу, что такое Lock Task (или Kiosk) Mode и как его настроить.
Что такое Lock Task Mode (LTM)?
Lock Task Mode (или Kiosk Mode) — это совокупность методов Android API, позволяющих заблокировать устройство в одном приложении.
Пользователь не может выйти из приложения, получить push от других приложений, а также выключить устройство.
LTM используется в ситуациях, когда:
Устройством пользуется большой поток пользователей, и нам нужно ограничить их возможности;
Компания выдает сотруднику устройство для работы и хочет, чтобы он был сосредоточен на рабочих задачах и не отвлекался на сторонние приложения.
Реализовываем 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
Прежде всего разрешаем 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>
Используем ActivityManager, чтобы убедиться, что мы еще не в LTM
C помощью DevicePolicyManager проверяем наличие нужных прав
Добавляем приложение в белый список. Все приложения, не находящиеся в нём, будут принудительно закрыты
Важно: если приложению необходимы сторонние процессы, их также следует добавить в белый список
-
Снова запускаем 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 можно воспользоваться паролем. Вариант решения:
При первом запуске генерируем UUID и сохраняем его в SharedPreferences
При последующих запусках достаём UUID из SharedPreferences
При старте отправляем запрос с UUID и дополнительной информацией для регистрации устройства в базе данных
При попытке выйти отправляем запрос с UUID для генерации пароля на сервер
Пользователь вводит пароль
Мы отправляем пароль с UUID на сервер, сервер отвечает, подошёл пароль или нет
В данном случае я использовал M2M (Machine-to-Machine) авторизацию — по сути обычную Auth0, только мы не вводим пароль, а сразу размениваем секрет на access token. Использовал я ее потому, что возможен случай выхода из LTM ещё неавторизованного пользователя. Если в приложение возможен выход только после авторизации, то можно использовать access token, полученный после авторизации. Помимо этого могут быть настроены другие способы аутентификации (например, apiKey).
Мой проект на GitHub: github.com/UserNameMax/kiosk-demo