Такие приложения, как Tasker и Locale, уже давно стали одним из главных аргументов в споре поклонников Android и iOS. Действительно, обычные, не требующие ни прав root, ни прав администратора приложения, а позволяют творить со смартфоном такое, что любой ветеран Linux-скриптинга обзавидуется. Как же они работают и почему подобных приложений нет в iOS и других мобильных системах? Все дело в Binder — IPC/RPC-механизме, пронизывающем весь Android.

 

Вместо введения

Создатели Android, имея возможность продумать архитектуру ОС с нуля, учитывая современные реалии, закономерно решили изолировать установленные приложения друг от друга. Так появилась идея использовать песочницы, поэтому Android-приложение: а) имеет доступ только к своему собственному каталогу и (если есть такие полномочия) к SD-карте — не содержащей важных данных свалке барахла, б) может использовать только заранее оговоренные возможности ОС (а в Android 6.0 пользователь может отозвать такие полномочия прямо во время работы программы) и в) не имеет права запускать, создавать каналы связи или вызывать методы других приложений.

Реализовано это все с помощью стандартных прав доступа к файлам (в Android каждое приложение работает с правами созданного специально для него пользователя Linux), многочисленных проверок на полномочия доступа к ресурсам ОС на всех уровнях вплоть до ядра и запуска каждого приложения в своей собственной виртуальной машине (сейчас ее уже сменил ART, но сути это не меняет).

Все эти механизмы отлично работают и выполняют свои функции, но есть одна проблема: если приложения полностью отрезаны друг от друга, вплоть до исполнения от имени разных юзеров, то как им общаться и как они должны взаимодействовать с более привилегированными системными сервисами, которые суть те же приложения? Ответ на этот вопрос — система сообщений и вызова процедур Binder, одна из многочисленных находок создателей легендарной BeOS, перекочевавшая в Android.

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

Сказанное может показаться тебе несколько странным, и, возможно, ты впервые слышишь о каком-то магическом Binder, но это благодаря тому, что Android абстрагирует программиста от прямого общения с этим драйвером (да, он реализован в ядре и управляется с помощью файла /dev/binder). Но ты точно имел дело с интентами, абстрактной сущностью, построенной поверх Binder.

 

Магические интенты

В Android объект класса Intent является абстракцией сообщения Binder и по большому счету представляет собой способ передачи управления компонентам своего или чужого приложения, вне зависимости от того, запущено приложение, к которому относится данный компонент, или нет. Банальнейший пример — запуск активности:

Intent intent = new Intent(this, SecondActivity.class);
startActivity(intent);
Процесс общения компонентов приложения через Binder
Процесс общения компонентов приложения через Binder

Это пример так называемого явного интента. Есть класс SecondActivity, в котором есть метод OnCreate(), а мы просто говорим системе: «Хочу запустить активность, реализованную в данном классе». Сообщение уходит, система его получает, находит метод OnCreate() в классе SecondActivity и запускает его. Скучно, просто, а местами даже тупо. Но все становится намного интереснее, если использовать неявный интент. Для этого немного изменим наш пример:

Intent intent = new Intent("com.my.app.MY_ACTION");
startActivity(intent);

И модифицируем описание активности в манифесте:

<activity android:name="com.my.app.SecondActivity" android:label="@string/app_name" >
  <intent-filter>
    <action android:name="com.my.app.MY_ACTION" />
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
</activity>

Результат будет тот же, а возни намного больше. Однако есть одно очень большое но: в данном случае мы использовали не имя компонента, который хотим запустить (SeconActivity), а действие, которое хотим выполнить (com.my.app.MY_ACTION), а формулировка «Я хочу запустить...» превратилась в «Я хочу выполнить такое-то действие, и мне плевать, кто это сделает». В результате нашу активность теперь можно запустить из любого другого приложения точно таким же способом. Все, что нужно, — это указать действие, все остальное система сделает сама.

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

Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:12345678900"));
startActivity(intent);

Этот простой код позволяет позвонить любому абоненту (при наличии полномочия android.permission.CALL_PHONE), а все благодаря тому, что приложение Phone умеет обрабатывать действие Intent.ACTION_CALL (точно таким же способом, как в нашем примере, с помощью intent-filter). Более того, если на смартфоне установлена сторонняя «звонилка», которая умеет реагировать на то же действие, система спросит, какое именно приложение использовать для звонка (а юзер может выбрать вариант, который будет использован в будущем).

Существует множество стандартных системных действий, которые могут обрабатывать приложения, например Intent.ACTION_VIEW для открытия веб-страниц, изображений и других документов, ACTION_SEND для отправки данных (стандартный диалог «Поделиться»), ACTION_SEARCH и так далее. Плюс разработчики приложений могут определять свои собственные действия на манер того, как мы это сделали во втором примере. Но одно действие обязаны обрабатывать все приложения, которые должны иметь иконку на рабочем столе, — это android.intent.action.MAIN. Среда разработки сама создает для него intent-filter и указывает MainActivity в качестве получателя. Так что ты можешь даже не подозревать о том, что твое приложение умеет его обрабатывать, но именно оно позволяет рабочему столу узнать, что на смартфон установлено твое приложение.

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

 

Широковещательные сообщения

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

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

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

Но, если приложение будет реагировать на сообщения от системы, эта проблема решится сама собой. Для реализации этого необходимо, чтобы приложение реагировало на сообщение CONNECTIVITY_CHANGE, а затем проверяло, что конкретно произошло: отключение от сети или подключение. Чтобы такое реализовать, нам понадобится ресивер. Для начала объявим его в манифесте, указав нужное сообщение в intent-filter:

<receiver android:name=".filters.ConnChangeReceiver"
  android:label="NetworkConnection">
  <intent-filter>
    <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
  </intent-filter>
</receiver>

Далее создадим сам ресивер filters/ConnChangeReceiver.java:

public class ConnChangeReceiver extends BroadcastReceiver {
  private String TAG="ConnChangeReceiver";

  @Override
  public void onReceive(Context context, Intent intent)
  {
    Intent myIntent = new Intent(context, MainService.class);
    if (NetTools.isConnected(context)) {
      Log.d(TAG, "Connected to network, start service");
      context.startService(myIntent);
    } else {
      Log.d(TAG, "Network disconnected, stop service");
      context.stopService(myIntent);
    }
  }
}

Ну и добавим в класс NetTools статический метод isConnected():

public static boolean isConnected(Context context) {
  ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
  if (cm == null) {
    return false;
  }

  NetworkInfo networkInfo = cm.getActiveNetworkInfo();
  if (networkInfo == null) {
    return false;
  }

  return networkInfo.isConnected();
}
Так выглядит код ресивера в реальном приложении
Так выглядит код ресивера в реальном приложении

Я даже не стал добавлять комментарии, тут и так все понятно: при возникновении сообщения CONNECTIVITY_CHANGE запускается ресивер, который проверяет, что произошло, коннект или дисконнект, и в зависимости от этого запускает или останавливает весь сервис целиком. В реальном коде есть еще куча других проверок, а также чекинг подключения с помощью пинга хоста 8.8.8.8 (подключение может быть установлено, но сам интернет недоступен), но это не так важно, а важно то, что простейший код позволяет нам легко избавиться от довольно серьезной проблемы, причем система по максимуму берет работу на себя, и, например, если сервис уже будет запущен к моменту его запуска, ничего не произойдет, второго сервиса не появится. Красота!

Таким же образом я могу запустить свой сервис при загрузке:

public class BootCompletedReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    Intent myIntent = new Intent(context, MainService.class);
    context.startService(myIntent);
  }
}

Строки из манифеста приводить не буду, скажу лишь, что в intent-filter надо указать действие android.intent.action.BOOT_COMPLETED.

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

Куча ресиверов в одном приложении
Куча ресиверов в одном приложении

[" anchor="primer"]

 

Пример в стиле ][

Конечно же, все эти сообщения можно использовать и в не совсем легальных целях. Многочисленные системные сообщения позволяют реализовать довольно хитрые схемы, например фотографирование включившего экран человека (действие android.intent.action.ACTION_SCREEN_ON), логирование звонков (NEW_OUTGOING_CALL и PHONE_STATE), логирование общего времени использования смартфона, да и вообще организовать полную слежку за действиями его пользователя. Плюс использовать возможности выполнить звонок или послать СМС без ведома пользователя.

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

public class PhoneStateReceiver extends BroadcastReceiver {
  private final String TAG = "PhoneStateReceiver";
  private boolean incomingFlag = false;
  private String incomingNumber = null;

  @Override
  public void onReceive(Context context, Intent intent) {
    if(intent.getAction().equals(Intent.ACTION_NEW_OUTGOING_CALL)){
      String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
      Log.d(TAG, "Call to number: " + phoneNumber);
    } else {
      TelephonyManager tm = (TelephonyManager)context.getSystemService(Service.TELEPHONY_SERVICE);
      switch (tm.getCallState()) {
        case TelephonyManager.CALL_STATE_RINGING:
          incomingFlag = true;
          incomingNumber = intent.getStringExtra("incoming_number");
          Log.d(TAG, "Incoming call: " + incomingNumber);
        break;
      case TelephonyManager.CALL_STATE_OFFHOOK:
        if(incomingFlag){
          Log.d(TAG, "Incoming call accepted: " + incomingNumber);
        }
        break;
      }
    }
  }
}

Как обычно, все элементарно, однако в этом случае ты можешь заметить, что мы не просто реагируем на интент, но и проверяем действие с помощью метода getAction() (наш ресивер должен реагировать и на действие NEW_OUTGOING_CALL, и на PHONE_STATE, то есть изменение состояния радиомодуля), а также проверяем переданные в интенте дополнительные данные с помощью getStringExtra(). А далее, если это исходящий звонок, просто логируем его, если же просто изменение состояния, то проверяем текущее состояние с помощью TelephonyManager и логируем, если звонок входящий.

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

 

Выводы

Теперь тебе должно быть предельно ясно, как работает Tasker. Все, что он делает, — это реагирует на широковещательные сообщения и в ответ отправляет другие сообщения (с помощью которых запускает софт или переключает те или иные настройки Android). Да, это опасная штука, с помощью которой написать шпиона, знающего о тебе все, проще простого, но в то же время и невероятно полезная для любого разработчика технология.

Тот же Google Now, например, умеет из коробки интегрироваться с любыми сторонними будильниками, мессенджерами и почтовыми клиентами именно потому, что использует Binder и стандартные действия типа ACTION_SEND. Точно так же любой разработчик может легко реализовать в своем приложении возможность сделать снимок камерой, выбрать файлы или отправить письмо, просто посылая нужные сообщения, которые будут обработаны стоковыми или другими приложениями, вне зависимости от того, какие из них предпочитает использовать юзер.

Оставить мнение