3. Широковещательные сообщение. Прием и отсылка сообщений.

Широковещательные сообщения – механизм оповещения приложений о наступлении определенных событий. Пользовательские приложения могут отправлять и получать сообщения от других приложений, а также могут получать сообщения от системных приложений Android. Также, широковещательные сообщения могут предназначаться другим компонентам этого же приложения (например, один из способов взаимодействия Service и Activity это обмен широковещательными сообщениями).

Дополнительная информация по поводу широковещательных сообщений доступна здесь и здесь.

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

Основные сценарии использования механизма широковещательных сообщений:

  • Реакция на системные события и на события устройства (основной сценарий использования широковещательных сообщений);

  • Обмен сообщениями между приложениями (как правило, оба приложения должны быть написаны вами, так как чтобы принять сообщение, необходимо точно знать его идентификатор);

  • Обмен сообщениями между компонентами одного приложения (обмен сообщениями между службами и activity или между службами).

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

Работа механизма широковещательных сообщений основана на модели publisher-subscriber («подписчик-издатель»), поэтому нам необходимо отдельно рассмотреть вопрос отсылки и приема широковещательного сообщения.

Модель «подписчик-издатель» в последнее время активно используется в «интернете вещей», в микросервисах, в различных протоколах вроде MQTT и технологиях обмена сообщениями вроде JMS и во многих других областях. Более подробно про эту модель читайте здесь и здесь.

Широковещательное сообщение и его отправка

Давайте сначала разберемся, что же собой представляет само сообщение. Оберткой для широковещательного сообщения служит объект класса Intent.

Очень важным параметром намерения является параметр action. В этом параметре указывается идентификатор сообщения (некоторая строка, как правило, сформированная определенным образом). Например для системного сообщения об изменении режима «airplane mode» идентификатором является строка «android.intent.action.AIRPLANE_MODE»). Именно идентификатор сообщения служит способом получить это сообщение в дальнейшем.

С помощью методов putExtra() и объекта Bundle можно передать данные в виде каких-то значений, но часто бывает достаточно самого факта прихода сообщения, чтобы ваше приложение отреагировало. Например, вместе с сообщением об изменении режима «airplane mode», система передает переменную, которая дает нам информацию о том, был ли режим «airplane mode» включен или выключен.

Как уже было сказано, механизм широковещательных сообщений реализуется средствами операционной системы, поэтому для отсылки сообщения нам необходимо иметь контекст, чтобы воспользоваться специальным методом sendBroadcast().

Таким образом, чтобы выслать широковещательное сообщение, необходимо выполнить следующие действия:

  1. создать объект типа Intent;

  2. с помощью метода setAction() указать action сообщения;

  3. с помощью методов putExtra() добавить данные (если они нужны);

  4. с помощью метода Context.sendBroadcast() выслать сообщение.

Создадим тестовый проект и по нажатию кнопки отправим широковещательное сообщение

MainActivity.java
public class MainActivity extends AppCompatActivity {

    private static final String ACTION ="ua.opu.pnit.bcapp.ACTION";
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button).setOnClickListener(v -> {
            Intent intent = new Intent();
            intent.setAction(ACTION);
            intent.putExtra("key","some_text");
            sendBroadcast(intent);
        });
    }
}

Приемник широковещательных сообщений.

Для приема широковещательных сообщений нам необходимо создать приемник широковещательных сообщений (Broadcast Receiver). Broadcast Receiver – это класс, который создается разработчиком, и чья задача состоит в получении широковещательного сообщения и реагировании на него.

Приемник ШС является отдельным компонентом приложения, так как может быть запущен отдельно от Activity, Service и Content Provider и может являться стартовой точкой приложения. В некоторых случаях, приемник ШС может быть запущен операционной системой и сам запустить стартовое Activity.

Для начала, создадим «заготовку» для приемника ШС. По традиции, для создания собственного компонента, нам необходимо отнаследоваться от стандартного класса приемника ШС, который называется BroadcastReceiver. Класс BroadcastReceiver является абстрактным, поэтому нам необходимо реализовать метод onReceive(), который будет вызван, когда приемник получит широковещательное сообщение.

MyReceiver.java
class MyReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        // Обработчик получения ШС
    }
}

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

Идентификатор, как вы помните, указывается с помощью метода setAction(). Таким образом, если вы хотите, чтобы ваше приложение А высылало сообщения приложению Б, тогда в сообщении из приложения A должен быть указан тот же идентификатор, на который должен подписаться приемник из приложения Б.

Приемник ШС можно использовать двумя способами: объявить в манифесте, зарегистрировать с помощью контекста. Эти два способа использования приемника существенно отличаются друг от друга. Рассмотрим каждый из этих способов.

Объявление приемника в манифесте приложения

Приемник, объявленный в манифесте, регистрируется при установке приложения и имеет жизненный цикл, независимый от других компонентов. Такой приемник может служить точкой входа в приложение. Если такой приемник получает сообщения, ОС запускает приложение и создает объект приемника ШС. Для каждого полученного сообщения, создается свой объект приемника. Как только метод onReceive() отработает, объект приемника уничтожается.

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

Чтобы объявить приемник в манифесте, необходимо указать элемент <receiver>, где указать класс приемника. Внутри элемента <receiver> необходимо указать элемент <intent-filter>, где необходимо указать, на какие широковещательные сообщения подписывается приемник.

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

Для начала создадим класс приемника широковещательных сообщений.

BootReceiver.java
public class BootReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {

        // Проверяем, какое ШС пришло в приемник
        if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {

            // Создаем Intent на запуск Activity
            Intent i = new Intent(context, MainActivity.class);
            i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            i.putExtra("message", "Hello!");

            context.startActivity(i);
        }
    }
}

Далее, заходим в манифест приложения, добавляем разрешение на прием сообщения типа ACTION_BOOT_COMPLETED, а также регистрируем приемник в теге <application>, при регистрации указываем, на какие события подписывается приемник.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="ua.opu.pnit.bcapp">

    <!-- Информируем о требуемом разрешении -->
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!-- Регистрируем приемник -->
        <receiver
            android:name=".BootReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>
    </application>

</manifest>

Далее редактируем содержимое класса MainActivity

MainActivity.java
public class MainActivity extends AppCompatActivity {

    private static final String ACTION = "ua.opu.pnit.bcapp.ACTION";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        String message = getIntent().getStringExtra("message");

        TextView textView = (TextView) findViewById(R.id.textView);
        textView.setText(message);
    }
}

Запускаем приложение, после чего перезагружаем устройство

Регистрация приемника в методах жизненного цикла контекста

Такой вариант регистрации является основным и целесообразен, если реакция на сообщение напрямую связана с этим контекстом. Например, мы хотим как-то отобразить в Activity факт прихода сообщения или какие-то данные, которые пришли к нам вместе с сообщением. Или сообщения каким-то образом влияют на работу запущенной вами службы.

Регистрацию приемника и снятие его с регистрации следует проводить в парных методах жизненного цикла (onCreate() - onDestroy(), onStart() - onStop(), onResume() - onPause() и так далее).

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

Для начала создадим класс приемника сообщений

MainActivity.java
class SmsReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals("android.provider.Telephony.SMS_RECEIVED")) {
            
            for (SmsMessage smsMessage : Telephony.Sms.Intents.getMessagesFromIntent(intent)) {
                String msg = "From " + smsMessage.getOriginatingAddress() + " : " + smsMessage.getMessageBody();

                Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
            }
        }
    }
}

Далее создадим в MainActivity поле класса - объект приемника. Регистрация будет происходить в методе onStart(), снятие с регистрации - в методе onStop().

MainActivity.java
public class MainActivity extends AppCompatActivity {

    private SmsReceiver receiver;
    
    ...
    
    @Override
    protected void onStart() {
        super.onStart();

        receiver = new SmsReceiver();
        IntentFilter filter = new IntentFilter();
        filter.addAction("android.provider.Telephony.SMS_RECEIVED");

        registerReceiver(receiver, filter);
    }

    @Override
    protected void onStop() {
        super.onStop();

        if (receiver != null)
            unregisterReceiver(receiver);
    }
}

Обратите внимание, что для регистрации приемника мы должны предусмотреть объект IntentFilter. Он необходим для указания параметра action тех сообщений, которые мы хотим получать.

Система разрешений в Android

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

Начиная с версии Android 6.0 (API Level 23), приложение должно явно запрашивать у пользователя одобрение на использование так называемых опасных разрешений (к которым относится получение SMS сообщений).

Для начала откроем файл манифеста и заявим, что приложению требуются следующие разрешения

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="ua.opu.pnit.bcapp">

    <uses-permission android:name="android.permission.READ_SMS"/>
    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
    
    ...

</manifest>

Далее, при старте MainActivity, затребуем от пользователя получение разрешений. Сначала происходит проверка, одобрил ли ранее пользователь требуемое разрешение. Если разрешение еще не получено, то затребуем разрешение от пользователя.

MainActivity.java
public class MainActivity extends AppCompatActivity {

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        requestSmsPermission();
    }

    private void requestSmsPermission() {
        if (checkSelfPermission(Manifest.permission.RECEIVE_SMS) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.RECEIVE_SMS}, 1);
        }
    }
}

Далее переопределяем метод onRequestPermissionsResult(), в котором запрограммируем реакцию приложения на ответ пользователя.

MainActivity.java
public class MainActivity extends AppCompatActivity {

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        requestSmsPermission();
    }

    private void requestSmsPermission() {
        if (checkSelfPermission(Manifest.permission.RECEIVE_SMS) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.RECEIVE_SMS}, 1);
        }
    }
    
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        if (requestCode == 1) {
            String msg = "Permission to receive SMS";
            msg += (grantResults[0] == PackageManager.PERMISSION_GRANTED) ? " granted" : " not granted";
            Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();
        }
    }
}

Запустим приложение

Дополнительные вопросы касающиеся темы широковещательных сообщений (явные и неявные сообщения, ordered-сообщения и так далее) выносятся на самостоятельное изучение. Ссылки на материалы предоставлены в начале темы.

Last updated