Kiosk Mode приложения на Android

Привет Хабр. Многие из нас каждый день трудятся на определёнными приложениями, создают что-то своё или выполняют требования нерадивого заказчика. Одним из таких требований может быть что-то вроде: «Хочу что бы из приложения нельзя было выйти, не хочу чтобы можно было делать что-то ещё, кроме как находится в моём приложений». В этом топике хочу поделиться мыслями, как же такое сделать.


Первое к чему мы приходим, что узнаём о новом типе приложений — Kiosk Mode — особый тип приложений, обычно работающие на устройствах в публичных местах. В таких приложениях функционал, к которому пользователь может иметь доступ ограничен самим приложением. Доступ к системе или каким-либо настройкам недопустим. Из таких приложений нельзя выйти. И так как мы здесь говорим об Андроиде, то, к большому сожалению, их API не предоставляет никаких возможностей для создания такого типа приложений. Попытаемся же разобраться, что со всем этим мы можем сделать и как хоть немного угодить нашему заказчику.

Хардварные кнопки и разъёмы


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

Панель с виртуальными кнопками


Начиная с версии Андроид 3.0 нам предлагают некоторую альтернативу хардварным кнопкам, такую как панель в нижней части экрана. Сюда входят кнопки «Назад», «Домой», «Опции», статус батареи и прочее.


Но для нашего приложения данная панель может очень мешать, так как позволяет выйти из приложения, войти в настройки системы и прочее, что может нарушить необходимый ход работы. Но есть один способ исчезнуть эту панель. Всё что будет описано здесь и далее, требует root прав на вашем устройстве.
И так, нам будет необходимо выполнит простую команду:
service call activity 79 s16 com.android.systemui

Данную команду может выполнить либо через adb, либо же напрямую из приложения:
Runtime.getRuntime().exec(new String[]{"su","-c","service call activity 79 s16 com.android.systemui"});


Это заставит враждебную для нас панель уйти с наших глаз. Но если вдруг эта панель будет нам нужна то вернуть её можно также просто командой:
am startservice -n com.android.systemui/.SystemUIService

Или же:
Runtime.getRuntime().exec(new String[]{"am","startservice","-n","com.android.systemui/.SystemUIService"});


Данное решение успешно работало на Андроид 3.0. Как обстоят дела с 4.0 пока сказать не могу.

Кнопки Home и Back


Если по каким-то причинам панель нужна, но нужно изменить поведение кнопок, то вот один рецепт. Начнём с простого, кнопки Back. Здесь всё легко, переопределяем метод:
	@Override
	public void onBackPressed() {
	    //Наше поведение...
	}


Теперь сложнее, кнопка Home. Google предусмотрительно отнёсся к этой кнопке, так как это единственный способ покинуть приложение и вернуться на главный экран, но для нас это беда, как раз именно это нам и не нужно. Что мы можем сделать:
  1. Нам необходимо добавить в AndroidManifest необходимые настройки для нашей стартовой активити:
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.HOME" />
                    <category android:name="android.intent.category.LAUNCHER" />
                    <category android:name="android.intent.category.DEFAULT" />
                </intent-filter>
    

    Это даст нам следующее, при нажатии на Home будет всплывать диалог:


    Уже неплохо, но как сделать чтобы этот диалог не появлялся, а открывалось наше приложение.
  2. Мы можем выбрать галочку, перезагрузить девайс и наше приложение будет запускаться по умолчанию, но это не тру вей, мы можем также легко снять эту галку в настройках системы. Пойдём по другому пути, мы найдём лаунечер в недрах операционной системы и просто переименуем его и/или отправим в другое место:
    mv /путь/Laucher.apk /путь2/LaucherOld.apk
    Всё, на этом основное приложение на устройстве одно — наше. Больше никакого всплывающего диалогов. Если же нужно иметь доступ к Launcher'у, то либо возвращаем его назад, либо создаём секретное меню и запускаем Laucnher оттуда куда мы его перенесли.


Проблемы


К сожалению, не всё так радужно как кажется. Не все вопросы решены до конца. К примеру, посмотрим на стандартную клавиатуру:


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


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

Различный системные диалоги


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




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

Заключение


В этой статье я постарался описать известные мне методы для создания Kiosk mode приложений. Если у хабрасообщества есть ещё какие-либо методы, то было бы здорово ими поделиться. Спасибо.
Поделиться публикацией

Комментарии 39

    +4
    Больше похоже на иструкцию, как сделать смс-локер. «Ваш планшет заблокирован! Т.к. на вашем планшете нет SIM, вам придется отправить СМС с вашего телефона на номер....»
    И кстати вы будете убирать ланчер из /system/app?
    Подкину вам идею, можно удалить также перемесить апдейтер…
    Только такие переносы в System/app череваты чем, что если кто-то сделает вайп у него не окажется ланчера например.
      0
      Чтобы сделать вайп придётся либо иметь доступ к хардварным кнопкам, либо к системным настройкам. Туда и туда доступ можно ограничить.
        0
        Вытаскиваем батарейку и входим в Recovery — делаем вайп.
          0
          Как если планшет в железном ящике закрытый на ключ? Можно отключить питание от ящика и подождать часов так 6-8 когда сядет батарейка. Согласен:)
            0
            Да, что то не подумал, однако если есть доступ к кнопкам, можно подержать кнопку выключения 10-20 секунд.
            0
            Да что уж — сразу уносим киоск к себе домой.
          0
          kiosk-mode эт полезная штука если вздумалось сделать какойнить информационный терминал в враждебной технике среде (например в универе :) ). В свое время активничал в этом направлении, но к сожалению так полной блокировки и не добился.
          +1
          Кнопка настроек на клавиатуре отключается… в настройках :)
            0
            Поподробнее, не нашёл такой опции в настройках, по крайней мере на ICS
              0
              Вот, пожалуйста — «Кнопка настроек». Только лонг-клика по space'у это не отменяет, так что все равно можно докопаться до настроек.
              Скриншот
                0
                Забавно, но на планшете такого нету, точнее нету галочки для кнопки настроек.
            0
            Вот для примера как это сделано (2 раза) на другой платформе:

            1) http://joris.kluivers.nl/blog/2012/03/02/kiosk-mode-for-ios/
            2) http://www.apple.com/ios/ios6/#accessibility
              0
              Вот такой штуки как по второй ссылке очень не хватает на андроиде…
              0
              Спасибо за информацию, но… это настолько сильно ломает UX и не соответствует общему стилю приложений, что совсем не понятно, в каких благородных целях стоит это делать.
                0
                Ну представьте, сервис терминалов оплаты мобилников. Представили? Теперь переносите это на планшет. Вы бы очень не хотели, чтобы в работу терминала каким-то образом можно было бы вмешаться. Согласны? Конечно, как вариант можно скачать исходники андроида, начать компилять и портировать всё, но а если на это нет времени, сроков и бюджета? Остаётся искать обходные пути…
                  +1
                  Это кстати не очень хорошо. Экран планшета не является антивандальным. Удар тяжелым предметом разнесет и сенсор и матрицу и все что под ними. Гораздо лучше для этого будет использовать маленький linux PC (в виде флешки например) и к нему уже сенсор с защитой и экран. Да и даже 10 дюймов будет маловато для терминала оплаты. А если смотреть по цене 10 дюймовый в общем то можно и за 100$ купить, но раздолбают очень быстро. Вроде за 25$ есть usb пк. А там уже и экран недорогой и тач под него думаю в 75 баксов можно уложиться, правда защита тача будет подороже стоить.
                    0
                    Полностью согласен. Мы думали об этом. Но мы выбрали именно уже готовый планшет с андроидом потому что во-первых времени на разработку первого прототипа уйдёт гораздо меньше, во-вторых планшет будет размещаться не на улице, а в закрытых местах, где он будет более-менее под присмотром. Вся вина будет лежать на том, кто разнесёт планшет.
                      +1
                      1. Так если специально подбирали планшет. Почему не подобрали, для которого уже есть дрова в аосп? Тогда бы вы спокойно могли менять все что захотите.
                      2. Зачем лепить «приложение», когда надо делать ланчер? Таким образом, сможете ограничить практически все, не прибегая к хакам и руту.
                        0
                        В последующем обязательно так и сделаем. На данный момент слепить приложение быстрее и проще.
                          +2
                          Ну только не приложение, а ланчер. Это и будет ваш киоск моде, из которого никуда не уйти, ничего не открыть.

                          Там конечно, костылей тоже своих хватит, но хотя бы рута не нужно. В качестве примера посмотрите на приложение Famigo Sandbox в маркете.
                      0
                      Я вам другой пример приведу — меню для ресторанов. Антивандальным его не сделать — планшет должен быть «стандартным», но и доступа никуда не должно быть.
                  0
                  Для терминала по оплате было бы куда проще сделать «антивандальную» рамку, закрывающую нижнюю панель. Раз уж доступ к хардварным кнопкам блокируется физически, то и это проблемой не должно стать. И выглядит куда проще :)
                    0
                    Это не терминал оплаты) Думали железно закрывать саму панель, но это не элегантно, как из пушки по воробьям, ещё уменьшает полезную площадь экрана, а при скрытии панели, как оказалось, площадь наоборот увеличивается.
                    0
                    а SYSTEM_ALERT_WINDOW разве не проще для этих целей?
                      0
                      Для каких именно целей? Не понял Вас…
                        0
                        для того чтобы отрисоваться поверх всех окон и не давать себя закрыть. приложения-локеры используют именно этот способ, например, play.google.com/store/apps/details?id=com.thinkyeah.smartlockfree
                          0
                          Спасибо. Попробую.
                      0
                      При наличии рута переносить системные аппы в другое место тоже не тру (на многих девайсах системный раздел вообще не перемонтируется на rw), к тому же при переносе может выполниться удаление приложение с потерей всех данных. Более правильно — отключать их через pm (package manager): pm disable com.android.launcher2. Соответственно, enable для включения обратно. Данные сохраняются, но приложение словно удалено, оно может встретиться лишь в управлении приложениями (если было отключено системное приложение).
                      Часть системных диалогов (в т.ч. список RecentApps) рисует процесс systemui в составе почти одноимённого пакета, так же успешно отключаемый.
                        0
                        Очень действенный метод. Скажите, а после ребута состояние заблокированных пэкеджей сохраняется?
                          0
                          Да, конечно. Статус сохраняется до активации (через pm) или сброса статуса всех приложений (например, rm -rf /data/system/*, или вайп). На нексусе, например, таким образом отключены гугл.земля, гугл.кошелёк, плей.видео и некоторые другие проприетарные сервисы, недоступные в текущем регионе.
                        0
                        Посмотрите как этот функционал реализован в SiteKiosk для Android:

                        www.sitekiosk.com/SiteKiosk/Default.aspx#android-tab

                        Может что-то Вам станет понятней.
                          0
                          Недавно столкнулся с проблемой — нужно было убрать злосчастный status bar в ICS и JB. Действительно, решение с «service call» годится только для HoneyComb. В ICS и других решение намного более ужасное:
                          "while 1; do killall com.android.systemui; sleep 1; done"
                            0
                            Какой ужас! Зачем старательно убивать процесс, восстанавливаемый системой, если можно его отключить (см выше). Хотя как ведёт себя отключение на планшетах, не знаю.
                              0
                              Да, как раз на планшетах все и непросто. В некоторых прошивках есть в настроках возможность отключить, но тут доступа к прошивке не было как раз, и в меню пункта такого не было. А «pm disable» не дает никакого эффекта — статус бар продолжает гордо выполнять свои функции. Очень надеюсь, что ситуацию изменят со временем. Потому что такое стыдно показывать даже :)
                                0
                                ну, если pm list доступен обычному пользователю, то pm enable/disable имеет результат только при выполнении с системными правами, а такое можно обеспечить рутом (и выполнять через su), или сделать свою программу системной, но для этого она должна иметь системный же uid в манифесте и системную подпись, что можно добиться сборкой всей системы вместе со своим приложением, или переподписью всей системы тем же сертификатом, что использовался для приложения. В любом случае получается, что нужен или рут, или кастомная прошивка, что не всегда возможно.
                                Хотя, если вы успешно вызывали kilall, значит, системные права уже есть (от пользователя системный процесс, понятно, не убить). Ещё замечу, что в некоторых случаях отключение этого пакета заметно после рестарта системы или, опять же, убийства процесса (но не факт).
                                  0
                                  Ой, простите, глупость напечатал. Конечно же while true, т.к в баше while 1 вовсе бессмысленно.
                                0
                                Решение описал ниже

                                0
                                Может кому поможет:

                                На ICS и выше для скрытия системного (навигационного) меню мне помог следующий код (нашел где то на stackoverflow):

                                try{
                                	//REQUIRES ROOT
                                	Build.VERSION_CODES vc = new Build.VERSION_CODES();
                                	Build.VERSION vr = new Build.VERSION();
                                	String ProcID = "79"; //HONEYCOMB AND OLDER
                                
                                	//v.RELEASE  //4.0.3
                                	if(vr.SDK_INT >= vc.ICE_CREAM_SANDWICH){
                                		ProcID = "42"; //ICS AND NEWER
                                	}
                                
                                	//REQUIRES ROOT
                                	Process proc = Runtime.getRuntime().exec(
                                		new String[]{"su","-c",
                                			"service call activity "+ ProcID +" s16 com.android.systemui"});
                                	proc.waitFor();
                                } catch(Exception ex) {
                                	Toast.makeText(getApplicationContext(), ex.getMessage(), Toast.LENGTH_LONG).show();
                                }
                                


                                Для запуска:
                                try {
                                	Runtime.getRuntime().exec(
                                		new String[]{"am","startservice","-n",
                                			"com.android.systemui/.SystemUIService"});
                                } catch (IOException e) {
                                	// TODO Auto-generated catch block
                                	e.printStackTrace();
                                }
                                
                                  0
                                  Спасибо большое за этот метод!

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

                                Самое читаемое