1 июня 2017

Когда нужен свой магазин приложений

Тема корпоративных приложений не первый год активно обсуждается на конференциях и в статьях. Все рисуют «бочку мёда»: быстро, всегда под рукой, из дома и офиса... В общем, перспективный, удобный инструмент для оперативного решения бизнес-задач.

Но давайте остановимся на одной «ложке дёгтя». Идиллическую картину может испортить вопрос распространения и обновления приложений среди сотрудников. Как показывает опыт, мало написать корпоративное приложение. Нужно ещё найти удобный способ доставить его до целевой аудитории, а затем делать обновления.

Мы в True Engineering написали некоторое количество корпоративных мобильных приложений, и всегда вопрос с установкой и обновлением вставал остро. Среди всех вариантов один нам кажется достаточно технологичным и интересным – свой магазин приложений. В посте поделимся деталями, как мы реализовали магазин под Android для пользователей корпоративных приложений.

А в чем, собственно, проблема?

Давайте подумаем, а есть ли проблема. Ведь существует несколько способов, как можно установить корпоративное приложение на Android:

Google Play

Если это Андроид приложение, давайте закинем его в стор и всё. Нет проблем! Так, да не совсем так. Во-первых, не все заказчики хотят видеть свои приложения для внутреннего использования в публичных магазинах.

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

Apple за такое точно бъёт по рукам. С Google дела обстоят попроще, строгого контроля нет. Но есть элементарная этика. Закидывание магазина такими приложениями не является хорошим поведением разработчиков.

Есть ещё и третья причина. Иногда компании имеют не одно корпоративное мобильное приложение. И удобно, когда их не нужно искать по официальному магазину приложений, а всё лежит в одном месте.

Сайт компании

Хорошо, давайте закинем все приложения на сайт и расставим ссылки. По этим ссылкам пользователь сможет скачать приложение с сайта. Вроде бы механизм работает, страничка красивая. Но как сообщить пользователю о необходимости обновить приложение?

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

MDM провайдеры

Для распространения в компании мобильных приложений идеально подходят MDM решения, коих сейчас на рынке не мало. Всё очень удобно. Скачиваешь MDM агент из магазина, авторизуешься в приложении и вуаля, смартфон сотрудника под управлением. Теперь с сервера на телефоны сотрудников можно устанавливать политики безопасности, разрешать и запрещать приложения из официального магазина, а также устанавливать корпоративные приложения. Звучит как мечта… но мечта дорогая.

На примере известного MDM решения AirWatch видно, во что это выливается компании - 4-6 долларов в месяц в расчёте на одно устройство.

Смущает, что необходимо платить ежегодно крупную сумму в расчёте на одного сотрудника. А если сотрудников много? Прямо скажем, получается недешёвое удовольствие.

F-Droid

Есть вариант адаптировать под свои нужды F-Droid, который доступен в исходниках (https://gitlab.com/fdroid). Но это решение тяжеловесное и использовать его для 2-3 приложений компании кажется избыточным.

Android for Work

Замечательно было бы пользоваться возможностями решения от Google. Но на данный момент это означает, что от компании-заказчика требуется, чтобы «организация включила сервис Android for Work и выбрала поставщика услуг по управлению мобильной инфраструктурой предприятия» (ссылка). То есть по сути снова получаем, что надо платить деньги провайдерам EMM решений.

Как видим, каждый из подходов оказался не достаточно хорошим. Это заставило нас собраться с мыслями, спроектировать и реализовать свой магазин приложений. О деталях реализации мы и расскажем дальше.

Обо всём по порядку

Чтобы магазин был полезным, мало просто показывать список доступных приложений. Нужно научить программу ряду привычных для пользователя действий, таких как: проверка наличия новых версий, загрузка и установка дистрибутивов приложений. К счастью, Android API предоставляет множество полезных инструментов для реализации подобных фич.

Получение списка доступных приложений

Пожалуй, самая простая часть :) Нужно договориться с сервером о формате и содержании данных, и научить его отдавать список с информацией о доступных приложениях. Все любят HTTPS/REST/JSON, они нам подходят.

Что касается содержания, то для каждого приложения мы сообщаем:

  • Идентификатор. AppId, обратная доменная нотация.
  • Название приложения.
  • versionCode. Целое число, обозначающее версию приложения. Пользователь не видит это число, но оно однозначно показывает, является ли текущая версия приложения последней.
  • versionName. Версия приложения, которая показывается пользователю. Может быть любой строкой, но обычно выглядит как "1.19.144"
  • Краткая информация о приложении.
  • Ссылка на иконку приложения. Без иконки никуда.
  • Ссылка на дистрибутив приложения. Она нам пригодится позднее, если пользователь решит установить/обновить это приложение.

Пример ответа от сервера:

 [
{ "id": "etr.appstore", "name": "App Store", "info": "Приложение для установки и обновления приложений компании", "versionName": "1.3", "versionCode": 6, "url": "https://myappstore.ru/appstore/etr.appstore.apk", "icon": "https://myappstore.ru/appstore/appstore/etr.appstore.android.png" }, { "id": "ru.eastbanctech.mobileregistrator", "name": "Мобильный регистратор", "info": "Регистрация пассажиров и услуга смены места, а также продажа услуг за местаи багаж на стойке регистрации", "versionName": "v1.1.5", "versionCode": 115, "url": "https://myappstore.ru/appstore/appstore/release.apk", "icon": "https://myappstore.ru/appstore/appstore/release.android.png" }]

Обратите внимание, что сам мобильный клиент магазина тоже находится в магазине приложений, что позволяет ему обновлять самого себя :)

Для решения такой задачи практически стандартом стало использование Retrofit2/OkHttp. RxJava - по желанию. Для отображения иконок из интернета мы используем Fresco от Facebook, потому что он уже с успехом используется нами на других проектах. Хотя Picasso может посоревноваться с Fresco и по функциональности, и по удобству использования. Кстати, мы уже используем Kotlin, так что все примеры в статье будут на нём.

Информация об установленных приложениях

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

val installedApps =
ontext.packageManager.getInstalledPackages(PackageManager.GET_META_DATA)

Эта команда вернет информацию об установленных приложениях в формате PackageInfo. Там можно найти много полезных данных, но нам пригодятся только packageName и versionCode.

Получив список установленных и список доступных приложений мы должны отобразить результирующий список приложений со всей объединенной информацией. Звучит как учебный пример какой-нибудь статьи по RxJava, неправда ли? ;) RxJava мы и воспользуемся:

    class RemoteAppModel // модель данных о доступном приложении в магазине
class DeviceAppModel // модель данных с информацией об установленном приложении val storeAppsObs: Observable<List<RemoteAppModel>> = appsService.getApps() val localAppsObs: Observable<List<DeviceAppModel>> = deviceService.installedApps() Observable.combineLatest(storeAppsObs, localAppsObs) { onlineAppList, localAppList ->   // объединяем два потока данных // превращяем список моделей от сервера в список результирующих моделей со всей нужной информацией onlineAppList.map { onlineApp -> // включаем в результат информацию о локальном приложении, если таковое имеется val localApp = localAppList.find { it.id == onlineApp.id } AppInfo(onlineApp, localApp) } }

Результирующий список уже показываем пользователю с помощью, к примеру, RecyclerView. Список доступных пользователю действий вполне очевиден:

  • если модель AppInfo не содержит localApp, значит приложение еще не установлено и его можно скачать/установить,
  • если в модели есть localApp, то нужно сравнить versionCode локальной копии и дистрибутива на сервере. При наличии новой версии опять же скачать/установить (обновить).

Установка приложения

С установкой приложений не все так однозначно. Эту задачу можно решить двумя способами: попросить пользователя установить дистрибутив или сделать это самостоятельно программно.

Первый подход реализуется довольно просто:

     //uri - путь до скаченного apk-файла
override fun install(uri: Uri) { val intent = makeInstallerIntent(uri) context.startActivity(intent) } private fun makeInstallerIntent(uri: Uri): Intent { val intent = Intent(Intent.ACTION_VIEW) intent.setDataAndType(uri, "application/vnd.android.package-archive") intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // на Android N и выше мы должны предоставить временное разрешение на доступ к файлу по Uri, // если этот файл находится в папке нашего приложения, а не в общедоступном разделе файловой системы val targets = context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) targets.forEach { info -> context.grantUriPermission(info.activityInfo.packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) } } return intent }

Запускаем intent и передаем управление в руки системного установщика дистрибутивов.

Второй способ установки потребует гораздо больше кода, но самая главная его проблема не в количестве кода, а в том, что программная установка apk файла требует наличия у программы привилегий android.permission.INSTALL_PACKAGES, получить которые может только "системное" приложение.

Ваше приложение может считаться системным в двух случаях:

  • приложение подписано тем же ключом, что и прошивка
  • приложение установлено в каталог /system/priv-app или /system/app в зависимости от версии Android. Установить приложение в системный раздел можно или с использованием root-доступа или установить тем же способом, каким устанавливают прошивки и моды: через bootloader и recovery mode.

Как вы понимаете, с установкой магазина вторым способом справится далеко не каждый пользователь. Однако, никто не мешает вам скомбинировать оба варианта: реализовать программную установку отдельным приложением-плагином. Клиент может проверять наличие установленного плагина и, при его наличии, делегировать установку ему, а в противном случае устанавливать дистрибутив привычным способом. Если вы решите идти по второму пути, вот несколько полезных ресурсов:

 
Проверка обновлений

Никто в здравом уме не будет каждый час открывать магазин и проверять наличие новых версий, телефон должен делать это самостоятельно и незаметно для пользователя. Android предоставляет сразу несколько способов выполнить периодическую фоновую работу. Мы провели анализ ряда популярных способов.

AlarmManager

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

Плюсы:

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

Минусы:

  • перестает работать после перезагрузки
  • перестает работать при выгрузке процесса "свайпом"
  • перестает работать при выключении через force close из настроек приложения

После перезагрузки можно попробовать восстановить работу, если слушать бродкаст BOOT_COMPLETE.

SyncAdapter и SyncService

Предназначен для синхронизации локальных данных и данных на сервере, т.е. как раз то, что мы хотим делать.

Плюсы:

  • не имеет перечисленных минусов AlarmManager
  • запускается только при наличии интернета (иногда это скорее минус :) )

Минусы:

  • нужно писать много boilerplate-кода, чтобы заставить его работать
  • рассчитан именно на периодическую фоновую работу, сложно настроить гибкое расписание фоновых работ
JobScheduler

Отличный фреймворк, объединяющий плюсы AlarmManager и SyncAdapter. Работает стабильно, эффективно и гибко настраивается. Имеет единственный недостаток: API 21+

Firebase JobDispatcher

Можно рассматривать как бэкпорт JobScheduler.

Плюсы:

  • JobScheduler, который работает на всех API

Минусы:

  • требует наличия GooglePlayServices на девайсе

Какой способ выбрать - дело индивидуальное. Мы придерживаемся схемы: SyncAdapter на pre-Lollipop и JobScheduler на API 21+.

Сервер

Поначалу мы умудрялись и вовсе обходиться без собственного сервера, используя в качестве бэкэнда Firbase Hosting. На хостинге лежали все apk файлы, иконки и json-файл со всей мета-информацией. Такая схема какое-то время работала, но имела ряд очевидных недостатков: никому не хочется вручную писать json-файлы и есть большой риск написать в него что-то не то.

Так что мы решили автоматизировать этот процесс и сделали свой сервер на базе NodeJS. Среди великого множества npm библиотек мы отыскали крайне полезный модуль app-bundle-info, который умеет доставать из apk файла versionCodeversionName и даже иконки приложения.

В итоге человеческий фактор при публикации приложения сведен к минимуму: сервер самостоятельно проверит валидность дистрибутива и самостоятельно достанет всю техническую информацию.

Вывод

Идея снабдить сотрудников компании мобильными приложениями хорошая и правильная, но при этом необходимо решить вопрос с доставкой приложений и поддержкой их актуальности.

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

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

Решение очень хорошо подошло для схемы COPE (Когда сотруднику выдается телефон компании, с уже проделанными предварительными шагами по настройке).

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

Всем хорошего дня и корпоративных приложений!