Tuesday, March 27, 2012

Первые впечатления от разработки под Андроидом — пишем handsfree

Совсем недавно обзавелся андрофоном (LG Optimus) и решил попробовать свои силы в написании софта под него. Почитав про устройство платформы сначала очень порадовался за ее простоту, удобство и логичность. Но на практике все оказалось далеко не так радужно…

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


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

  1. В манифесте вешаем receiver на сообщения об изменении статуса линии (TelephonyManager.ACTION_PHONE_STATE_CHANGED) и следим за входящими звонками.
  2. При поступлении звонка проверяем, подключена ли гарнитура. Меня интересовала, прежде всего, bluetooth, но и проводную гарнитуру было бы хорошо отслеживать.
  3. Если гарнитура подключена, говорим телефону ответить на звонок.

С первым пунктом особых проблем не возникло. А вот остальные два оказались не такими тривиальными.

Ответ на звонок


Начну с третьего пункта. В Андроиде нет API для ответа на входящий звонок. Если в версиях 1.х еще можно было достучаться до каких-то недокументированных классов и методов, которые позволяли это, то в моем 2.2 все эти дыры надежно закрыли. Изрядно погуглив нашел-таки способ: изобразить нажатие кнопки на гарнитуре (Intent.ACTION_MEDIA_BUTTONKeyEvent.KEYCODE_HEADSETHOOK). Правда, тут есть свои тонкости. Событие это надо посылать через sendOrderedBroadcast (не sendBroadcast). Иначе оно попадет всем receiver-ам, а среди них может оказаться, например, аудиоплеер, который бодренько запустится, если найдет что играть. Интересно, что в эмуляторе плеер запускается даже при использовании ordered broadcast, хотя на телефоне все работает как надо. Еще одно отличие эмулятора от железа: в первом достаточно послать сообщение об отпускании кнопки (кстати, плеер запускается только по сообщению о нажатии). В телефоне нужно обязательно сгенерировать и нажатие, и отпускание. Говорят, в 1.х можно было докопаться до функции injectKeyEvent (или какой-то подобной) и это было бы правильнее. Но в 2.2 защиту этой функции починили и она недоступна.

Состояние гарнитуры


Теперь самое сложное — пункт 2. Проверить, подключена ли гарнитура. Проблема та же — в Андроиде нет готового API, позволяющего узнать это. И что еще интереснее, действия для проводной и беспроводной гарнитур будут заметно отличаться.

В принципе, для обеих гарнитур можно отслеживать сообщения о подключении/отключении, с чего я и начал. Для проводной это Intent.ACTION_HEADSET_PLUG (состояние гарнитуры передается как extra-параметр), для беспроводной — пара сообщений BluetoothDevice.ACTION_ACL_CONNECTED иBluetoothDevice.ACTION_ACL_DISCONNECTED.

Первое, что хочется сделать — зарегистрировать receiver-ы этих сообщений прямо в манифесте, чтобы ничего не пропустить. С беспроводной гарнитурой этот номер проходит, а вот ACTION_HEADSET_PLUGпочему-то рассылается с флагом FLAG_RECEIVER_REGISTERED_ONLY, то есть receiver-ы из манифеста его не получают (в документации про это ничего не сказано, так что очень похоже на ошибку). Надо их регистрировать в коде. А это значит, что придется запустить сервис, который программно зарегистрирует нужный receiver и будет оставаться все время запущеным. Тут, однако, тоже есть свои грабли: такой сервис может быть убит системой в любой момент (что и происходило у меня в процессе тестирования), в результате чего мы можем пропустить какие-то сообщения. Судя по всему, подобный подход использован в том единсвенном приложении из маркета, которое я упоминал в начале статьи — оно как раз не всегда вовремя определяло подключение гарнитуры. Приходилось запускать его руками.

Выход был найден. На помощь приходит тот факт, что сообщение ACTION_HEADSET_PLUG является sticky. То есть, последнее состояние гарнитуры сообщается всем вновь регистрируемым слушателям. Получается, состояние беспроводной гарнитуры можно проверить в любой момент, вызвав registerReceiver(null, headsetPlugFilter) и проверив вернувшийся Intent. Это радует. Сервис не нужен, пункт 2 для проводной гарнитуры реализован.

Переходим к беспроводной гарнитуре. Тут уже точно придется отслеживать сообщения о подключении/отключении — текущее состояние гарнитуры в произвольный момент проверить никак нельзя. Зато можно использовать манифест для регистрации receiver-а. Еще одно неудобство — текущее состояние надо где-то хранить, а использовать обычную переменную для этого нельзя, поскольку между звонками никакой код не выполняется. Я выбрал для этой цели скрытый параметр в настройках приложения. Скрытый в том смысле, что он не отображается в окне с настройками (это окно я не буду рассматривать — там все просто).

Call waiting


Чуть не забыл про еще один важный момент. Если человек уже разговаривает по телефону, то, скорее всего, ему не нужно, чтобы новые входящие звонки отвечались автоматически. Я сделал для этого отдельную опцию в настройках. Соответственно, прежде чем ответить, мы должны проверить, не было ли у нас активного разговора в момент звонка. И снова API не предоставляет для этого никаких средств. Все, что имеется — уже работающий receiver для звонков. Мы можем отслеживать изменение состояния телефона в offhook или idle. Но использовать для хранения этого состояния насройки приложения (как для состояния беспроводной гарнитуры) может оказаться уже довольно накладно. Поэтому снова нужен работающий сервис, который будет хранить состояние offhook в памяти. А чтобы уменьшить вероятность убивания сервиса системой в неподходящий момент, можно на время запустить его через startForeground. Когда звонки завершены, сервис можно приостановить, вызвав stopForeground.

Впечатления


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

Кстати, в моем исходном плане была еще опция объявлять звонящего используя TextToSpeech. Но оказалось, что перенаправить звук от TTS в беспроводную гарнитуру в момент звонка нельзя (если кто-то знает способ, поделитесь), а говорить в телефон, лежащий в кармане, смысла особого нет. Не стал заморачиваться — просто выкинул эту функцию целиком. Хотя, конечно, жаль немного.

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


©HabraHabr.ru