IT-инфраструктура для бизнеса и творчества
Разработка
Axmor Software

Как сделать корпоративное мобильное приложение единственным на устройстве с помощью Android Management API

Далеко не всегда устройства с операционной системой Android используются как персональный гаджет. Иногда они служат одной конкретной бизнес-цели, и необходимость в предустановленных свойствах девайса отпадает.

В этой статье рассказываем, как мы в Axmor используем несколько простых манипуляций с Android Management API, чтобы заблокировать ненужные функции без root-прав.

SingleApp-устройства можно встретить в ресторанах, аэропортах, гостиницах, а также в корпоративной сфере​

Каждый из нас хоть раз в жизни имел дело с корпоративными девайсами:

  • в случае клиентских приложений они используются для покупки билетов, заказа еды в ресторанах, регистрации гостей и т.д;

  • в случае корпоративных — это могут быть приложения для менеджеров по продажам, курьеров, логистов, системы управления складскими запасами и т.д.

При такой эксплуатации часто требуется ограничить устройство одним приложением. Иногда для этих целей используются специализированные компьютеры, но по сути любой планшет или смартфон можно превратить в то, что в документации по Android-разработке называют Dedicated device.

В нашей компании разрабатывается много enterprise-приложений, из-за чего мы часто сталкиваемся с необходимостью блокировки функций на dedicated devices. Например, в нашей практике было приложение для инженеров по безопасности на промышленных предприятиях. Эти специалисты ходят по заводу, следят за исправностью трубопроводов и должны куда-то заносить собранные данные:

Компания-заказчик закупила для этой цели несколько десятков смартфонов на Android и заказала нам приложение со следующими требованиями:

  1. на экране всегда должно присутствовать выбранное приложение без возможности запуска других приложений. Соответственно, должны быть заблокированы кнопки “Назад”, “Домой” и “Последние приложения”;

  2. выбранное приложение должно запускаться сразу же после перезагрузки;
  3. заблокировать возможность изменять какие-либо настройки на устройстве;
  4. заблокировать возможность взаимодействия со статус-баром, т.к. там возможно менять настройки;
  5. заблокировать возможность открывать пуш-уведомления;
  6. экран устройства не должен гаснуть.

Часть этого функционала можно реализовать довольно просто, однако на текущий момент устройство без root-прав не позволяет как-либо блокировать взаимодействие со статус-баром и пуш-уведомлениями. Также не удастся ничего сделать с кнопкой “Последние приложения”.

Тут на помощь приходит Android Management API и device policy controller (DPC), которые позволят выполнять настройку корпоративных устройств в кратчайшие сроки и по своему усмотрению блокировать любые функции.

Инструменты для управления устройством: Device policy controller и Android Management API

DPC — это приложение, которое может контролировать разрешения других приложений, а также блокировать различные функции устройства. Соответственно, используя кастомный DPC, мы можем в том числе скрывать статус-бар и блокировать системные кнопки на устройстве.

Написание собственного DPC довольно трудозатратно, т.к. включает в себя написание целого приложения, единственная функция которого — ограничивать возможности пользователя при работе с устройством. Кроме того, любые изменения в политике устройства придётся внедрять в коде, что повлечет за собой выпуск новой версии этого приложения.

Чтобы упростить жизнь разработчикам таких решений, Google в 2017 году выпустил свой DPC, который называется Android Device Policy и управляется с помощью Android Management API. Чтобы использовать Android Management API, нужно выполнить несколько шагов:

  1. Создать сервисный аккаунт. Этот шаг является опциональным и нужен для вызова методов API из кода (с бэкенда или из web-консоли, которую также нужно писать самим), поскольку такие вызовы требуют аутентификации через OAuth.
  2. Создать организацию (enterprise). Как следует из названия, это ресурс, который представляет собой организацию. Чтобы создать такой ресурс, нужен Gmail-аккаунт, не привязанный ни к какой организации. Enterprise состоит из политик (policies) и устройств (devices).
  3. Создать политику (policy). Это тот самый набор правил и ограничений, который будет применен к устройству. Когда мы изменяем что-либо в policy, все устройства, к которым она применена, автоматически обновляют своё состояние в соответствии с изменениями без каких-либо дополнительных действий с нашей стороны.
  4. Зарегистрировать устройство (device). Чтобы применить созданную политику к устройству, нужно создать EnrollmentToken, привязанный к выбранной policy. Затем устройство нужно сбросить к заводским настройкам, т.к. установить DPC в качестве Device owner’а можно только в момент первичной настройки системы. Это работает для устройств с ОС Android 6.0 и выше, подробности можно найти в официальной документации. Самый простой способ передать токен на устройство — сформировать QR-код на его основе. JSON для формирования QR-кода можно получить из поля qrCode у ресурса EnrollmentToken. Чтобы отсканировать QR-код, нужно тапнуть 6 раз в одном и том же месте на первом экране, появившемся после сброса заводских настроек (обычно на нём мы выбираем язык), после чего будет предложено подключиться к интернету и скачать сканер QR-кодов. Если же этот способ не сработал на выбранном устройстве, можно дойти до этапа ввода данных Google-аккаунта и ввести туда afw#setup.

Google предоставляет несколько опций, позволяющих пользоваться Android Management API. Самый простой способ выполнить все эти шаги — пройти интерактивный quickstart guide. Для обращения к API из кода можно использовать одну из библиотек на Java, .NET, Python, или Ruby.

Использование Android Management API для создания SingleApp-устройства

Вернёмся теперь к списку требований для Dedicated device и рассмотрим, как их можно реализовать с помощью policy. Чтобы выбранное приложение всегда присутствовало на экране и запускалось автоматически после перезагрузки, оно должно заменить стандартное Home app. Для этого нужно добавить следующий блок в политику:

"persistentPreferredActivities": [ { "actions": [ "android.intent.action.MAIN" ], "categories": [ "android.intent.category.HOME", "android.intent.category.DEFAULT" ], "receiverActivity": "com.example.lockerApp" } ]

Также нужно разрешить приложению запускать lockTaskMode, который позволит нам скрыть кнопки “домой” и “последние приложения”, а также не позволит выйти из приложения с помощью системной кнопки “Назад”:

"applications": [ { "defaultPermissionPolicy": "GRANT", "lockTaskAllowed": true, "packageName": "com.example.lockerApp" } ]

Чтобы запустить lockTaskMode, просто вызовите в коде основного Activity своего приложения соответствующий метод:

fun startLockTask() { val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager if (am.lockTaskModeState == ActivityManager.LOCK_TASK_MODE_NONE) { super.startLockTask() } }

Чтобы скрыть статус бар, нужно добавить в policy

"statusBarDisabled": true

Ну а для того, чтобы экран всегда оставался включенным, нужно просто добавить флаг android:keepScreenOn="true" в корневую ViewGroup Activity. Итоговый JSON политики будет выглядеть примерно так:

{ "adjustVolumeDisabled": true, "appAutoUpdatePolicy": "ALWAYS", "applications": [ { "defaultPermissionPolicy": "GRANT", "installType": "FORCE_INSTALLED", "lockTaskAllowed": true, "packageName": "com.example.lockerApp" } ], "persistentPreferredActivities": [ { "actions": [ "android.intent.action.MAIN" ], "categories": [ "android.intent.category.HOME", "android.intent.category.DEFAULT" ], "receiverActivity": "com.example.lockerApp" } ], "playStoreMode": "WHITELIST", "safeBootDisabled": true, "screenCaptureDisabled": true, "statusBarDisabled": true, "systemUpdate": { "endMinutes": 240, "startMinutes": 120, "type": "WINDOWED" } }

Различные примеры политик можно посмотреть тут. А все возможные параметры для политики — тут.

Есть ещё одна проблема, с которой мы столкнулись при разработке нашего решения. В одном из проектов Axmor заказчику требовался функционал выхода из заблокированного режима, чтобы можно было снова полноценно пользоваться устройством без сброса к заводским настройкам. Это можно сделать через создание двух разных политик — policy_locked и policy_unlocked, и инициируя их смену через backend с устройства. Но дело в том, что с устройства нельзя напрямую получить его внутренний id в Android Management API, а значит — нельзя выполнить никакие операции с ним. До Android 10 можно найти устройство в общем списке по его серийному номеру, который хранится в Device.hardwareInfo.serialNumber, сравнив его с серийным номером, полученным через Build.getSerial() или Build.SERIAL, однако начиная с Android 10, получение серийного номера запрещено для несистемных приложений.

По этой проблеме есть ответ на Stackoverflow от Google. Они советуют использовать managed configurations, фактически — это дополнительные параметры, которые можно передать на устройство, прописав их в policy. Конечно, в таком случае для каждого устройства нужна своя уникальная policy. Для того, чтобы избежать ручного ввода, можно использовать Pub/Sub нотификации, которые позволяют получить уведомление при регистрации нового устройства и сразу же записать в managed configurations его id. Не самый простой способ, но самый правильный. Прописать какой-нибудь уникальный id для устройства вручную при формировании QR-кода не получится, потому что managed configurations устроены таким образом, что записать туда что-либо возможно только после того, как устройство уже зарегистрировано.

Как опубликовать enterprise-приложение в Google Play

Итак, у нас есть policy, способная заблокировать всё и вся на устройстве, кроме нашего избранного приложения. Дело сделано? Не совсем. Наше приложение ещё требуется как-то установить на устройство, а просто так выкладывать в Google Play не очень хочется — ведь мы явно делаем его не для широкой аудитории. Можно, конечно, отправить заказчику apk и попросить поставить приложение вручную, но делать это для каждого устройства довольно утомительно, плюс это нужно будет делать при каждом обновлении приложения.

Решение — использовать Managed Google Play и выложить наше приложение как private app. Для этого нужно сначала загрузить приложение в Google Play, затем зайти в Store Presence -> Pricing & Distribution -> User programs -> Managed Google Play, отметить Turn on advanced managed Google Play features -> Privately target this app to a list of organizations. Здесь нужно выбрать id enterprise, для которой мы регистрируем устройства. Теперь, если в policy для нашего приложения будет прописано поле installType как FORCE_INSTALLED, оно будет установлено автоматически при регистрации устройства.

Заключение

Несмотря на некоторые недостатки, Android Management API в настоящий момент — самый простой и удобный способ для создания Dedicated device или устройства одного приложения в ОС Android. Большие возможности для кастомизации позволяют делегировать непосредственное выполнение кода для создания ограничений на устройстве готовому DPC, что способно существенно сократить время разработки. Нам на мобильных проектах в Axmor это API позволяет сократить время на выполнение “рутинной” работы по реализации ограничений и уделить больше внимания самому Single App.

(function () { let cdnUrl = `https://specialsf378ef5-a.akamaihd.net/SelectelBranding/images/` let previousArticleNumber = null let currentArticleNumber = 0 let platform = 'Desktop' let articles = [ { name: 'camera', url: `${cdnUrl}CameraCat`, text: 'умную камеру для\u00A0наблюдения за\u00A0котиками', link: 'https://vc.ru/selectel/306690', num: 3 }, { name: 'chill', url: `${cdnUrl}ChillCat`, text: 'трекер, который подскажет, когда пора отдохнуть', link: 'https://vc.ru/promo/288561-eye-tracker', num: 1 }, { name: 'cloud', url: `${cdnUrl}CloudCat`, text: 'котика: даёшь ему «пять», а\u00A0он делает бэкап в облако', link: 'https://vc.ru/dev/294799-maneki-neko', num: 2 } ] let buttonCycle = document.querySelector('.button--cycle') let buttonChoose = document.querySelector('.button--choose') let buttonMobile = document.querySelector('.button--mobile') let textField = document.querySelector('.selectel-footer-subtitle') let imageAgent = document.querySelector('.image--agent') let banner = document.querySelector('.selectel-footer') buttonCycle.addEventListener('click', cycleClick) buttonChoose.addEventListener('click', () => sendEvent(`Promo ${articles[currentArticleNumber].num} Left`, 'Click')) buttonMobile.addEventListener('click', () => sendEvent(`Promo ${articles[currentArticleNumber].num} Left`, 'Click')) let media = window.matchMedia("(max-width: 570px)") media.addEventListener('change', matchMedia) function matchMedia() { if (media.matches) { platform = 'Mobile' } else { platform = 'Desktop' } update() } matchMedia() function cycleClick(event) { sendEvent(`Promo ${articles[currentArticleNumber].num} Right`, 'Click') if (event) { event.preventDefault() event.stopPropagation() } window.open('https://vc.ru/tag/selectelDIY', '_blank') //cycle(event) } function cycle(event) { // incrementArticleNumber() textField.innerHTML = generatedText() imageAgent.src = articles[currentArticleNumber].url + platform + '.svg?3' imageAgent.setAttribute("class", "") imageAgent.classList.add('image--agent', articles[currentArticleNumber].name) banner.href = articles[currentArticleNumber].link } function update() { banner.href = articles[currentArticleNumber].link imageAgent.src = articles[currentArticleNumber].url + platform + '.svg' textField.innerHTML = generatedText() } function incrementArticleNumber() { previousArticleNumber = currentArticleNumber if (currentArticleNumber >= articles.length - 1) { currentArticleNumber = 0 } else { currentArticleNumber++ } } const sendEvent = (label, action = 'Click') => { const value = `SelectelDIY — loc: Footer — ${label} — ${action}`; if (window.dataLayer !== undefined) { window.dataLayer.push({ event: 'data_event', data_description: value, }); } }; function generatedText() { let defaultText if (platform === 'Desktop') { defaultText = `Мы тут собрали %text%. Хотите научим?` } else { defaultText = `Мы тут собрали %text%.` } return defaultText.replace('%text%', articles[currentArticleNumber].text) } function getRandom(min, max) { min = Math.ceil(min) max = Math.floor(max) return Math.floor(Math.random() * (max - min + 1)) + min } (function create() { currentArticleNumber = getRandom(0, articles.length - 1) cycle() let page = document.querySelector('.page--entry') if (page) { function insertAfter() { let parents = page.querySelectorAll('[data-id="7"]') let referenceNode = parents[0] referenceNode.parentNode.insertBefore(banner, referenceNode.nextSibling); loaded() } setTimeout(() => insertAfter(), 0) } }()) function loaded() { banner.classList.add('loaded') } loadImages([ `${cdnUrl}CameraCatDesktop.svg`, `${cdnUrl}ChillCatDesktop.svg`, `${cdnUrl}CloudCatDesktop.svg`, `${cdnUrl}CameraCatMobile.svg`, `${cdnUrl}ChillCatMobile.svg`, `${cdnUrl}CloudCatMobile.svg?3`, ]) function loadImages(urls) { return Promise.all(urls.map(function (url) { return new Promise(function (resolve) { var img = document.createElement('img'); img.onload = resolve; img.onerror = resolve; img.src = url; }); })); } }())
0
6 комментариев
Популярные
По порядку
Написать комментарий...
Максим Куликов

Даже не слышал про такую опцию, спасибо за вводную.

Ответить
2
Развернуть ветку
Sergey Mosalov

Спасибо! Это было полезно.

Ответить
0
Развернуть ветку
Axmor Software

рады поделиться! 

Ответить
0
Развернуть ветку
Patrick Robinson

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

Ответить
0
Развернуть ветку
Lubyagov Nickolay

Не проверял, чисто теория у меня такое ощущение, что PUB/SUB нотификации можно отправить из самого приложения, через https://developer.android.com/reference/androidx/enterprise/feedback/package-summary оттуда, добыть устройство, и поменять ему полиси на версию с не прикрепленным экраном. Без выделеных policy для каждого устройства. Вопрос в другом, а что делать если интернета в этот момент нет, и нужно как-раз эту проблему решить, открипив экран? Я это решил вызовом интента, для доступа к настройкам устройства, и VPN клиенту, из самого приложения.

Еще вариант полиси с опцией "kioskCustomLauncherEnabled": true оно всё-же позволяет по желанию приложениям кодом, прикреплятся к экрану (не знаю может это баг). Только работает это странно, после загрузки устройства, через 30 секунд, экран приложения открепляется сам! И выпадает на рабочий стол(кастомный ланчер), так-же экран открепляется при смене локали устройства (а у меня кстати задача менять локаль устройства, из настроек на сервере. Кстати Management API менять локаль не позволяет, покрайней мере полиси я не нашел, а вот было бы круто). Хотя потом можно снова запустить приложение и оно прикрепит экран.

Еще вопрос, а что делать, если я после установки Single APP приложения, хочу зарегистрировать аккаунт, сразу для него. Хорошо есть Pub/Sub нотификации, в конце установки. Затем можно задать этот аккаунт в приложение, через managed configurations...
Но блин, вся прелесть полиси, в том что изменив один раз policy, мы получаем эффект на всех устройствах с этим полиси (например установить новое приложение на все устройства), и это ломается, т.к. идентификатор управляемой конфигурации или сама конфигурация, задаётся в Policy. А вот аккаунт желательно б иметь на каждом устройстве свой.

Ответить
0
Развернуть ветку
Lubyagov Nickolay

да еще замечу:
```
lockTaskAllowed
(deprecated)
boolean

This item is deprecated!

Whether the app is allowed to lock itself in full-screen mode. DEPRECATED. Use InstallType KIOSK or kioskCustomLauncherEnabled to to configure a dedicated device.
```
Понятно, что можно было б просто указать lockTaskAllowed, и приложение оставить с FORCE_INSTALLED приложение с описанным BOOT_COMPLETED рецейвером. Но вот незедача, устарело! И кстати опция упомянута в статье.

Ответить
0
Развернуть ветку
Читать все 6 комментариев
OZON заблокировал аккаунт и отказывается объяснять причину

Ничего не предвещало беды. Как это обычно и бывает, я пришел в ПВЗ, чтобы забрать свой заказ, достал телефон, чтобы назвать сотруднику номер заказа, и столкнулся с проблемой: меня выкинуло из аккаунта. При попытке авторизоваться по номеру телефона или по почте я получил ошибку:

Почему начинающим предпринимателям стоит выходить на маркетплейсы

Торговые онлайн-площадки готовы сотрудничать с бизнесом любого размера.

В «День шопинга» продажи косметики в онлайне выросли в 4 раза

Доля выручки от интернет-заказов в сети «Золотое яблоко» в ноябре составила 45%. Это на 15% больше относительно предыдущих месяцев года.

Праздник к нам приходит: когда заказывать подарки за рубежом

Ещё в ноябре для логистических компаний начинается горячий сезон, который продолжается до марта. 11 ноября — большая распродажа AliExpress, а сразу после неё — Чёрная пятница, плавно перетекающая в подготовку к Новому году, 14 и 23 февраля, и, наконец, к 8 марта. В это время Почта и другие логистические службы по всему миру доставляют особенно…

Эвакуатор для своих: бармен из Санкт-Петербурга придумал способ заработать, когда закрыли общепит Статьи редакции

Он сам переоборудовал грузовик Toyota Dyna и принимает заказы от владельцев автосервисов и знакомых автомобилистов.

Платформу для эвакуатора варили из металла вручную
Для пользователей WhatsApp в США запустят мгновенную отправку криптовалюты — без комиссий и ограничений Статьи редакции

Нововведение позволит пользователям «быстро и безопасно» получать и отправлять криптовалюту.

Пользователи Авито спасли от вырубки леса равные по площади территории Карелии

Как деятельность Авито влияет на сокращение загрязнения окружающей среды? Перепродажа наиболее вредных с точки зрения производства товаров помогла пользователям Авито за 2020 год совместно сэкономить более 1,3 миллиарда кг различных материалов, 267 млн ГДж энергии и 123 млн м3 воды.

Благодарность команде vc.ru

Эти ребята делают наш мир лучше.

Как поднять продажи фармы в диджитале? Рассказываем про стратегию продвижения бренда «Ультра-Д» и показываем результаты

В пандемию фармотрасли тоже пришлось пойти в диджитал, но из-за ограничений ей это дается сложнее, чем остальным. Как построить стратегию продвижения фармпродукта в онлайне и поднять продажи, не нарушив закон? В 2020-м мы проверили в бою собственную стратегию продвижения фармы — а теперь рассказываем, как она работает и какие дает результаты.

Голова не варит: 10+ советов, как предотвратить умственное переутомление

Наш мозг потребляет уйму энергии, но мы почему-то часто игнорируем, продолжая нагружать его новыми задачами. Как следствие — возникает умственное переутомление, и мы печально и бессильно глядим в пустоту в конце дня. Разбираемся, почему такое состояние возникает и как с ним бороться.

Tmall заморозил 80к руб. и не отправляет заказ 16 дней

Приветствую! Мой первый пост на vc, не пинайте сильно. Постараюсь кратко и по факту.

null