Освой Android играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

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

Передача сообщений
Приёмники широковещательных сообщений
Периодическое срабатывание каждую минуту
Другой вариант
Автостарт Activity или Service при загрузке (перезагрузке) девайса
Следим за питанием

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

Передача сообщений

Для начала научимся отправлять сообщения. В одном из уроков мы учились запускать другую активность с помощью намерения Intent. Но намерения можно использовать для отправки сообщений, предназначенные не какому-то отдельному приложению, объекту или компоненту, а всем. И любая программа, оборудованная специальным приёмником, может поймать это сообщение и предпринять свои шаги на основе полученной информации.

Для понимания, представьте, что радистка Кэт отправляет сообщение: "Срочно пришлите кота! Хочу быть сильной независимой женщиной. А ваш Штирлиц - фашист!". Возможно в этом сообщении содержится шифровка, что нужно прислать жену, столик для жены разведчика в кафе заказан, а Штирлиц - козёл! Но это не важно для нашего урока.

Любой человек, имеющий специальный оборудованный радиоприёмник, может принять это сообщение. Так же поступают и программы. Они обзаводятся приёмниками и прослушивают определённый тип сообщений.

Сообщения может создавать сама система, а также ваша программа и чужие программы.

Передача сообщений весьма проста в реализации. В вашем приложении необходимо создать сообщение, которое вы хотите передать. Установите при необходимости поля action, data и category (действие, данные и категорию) вашего сообщения и путь, который позволяет приёмникам широковещательных сообщений точно определять "своё" сообщение. В этом сообщении строка действия ACTION должна быть уникальной, чтобы идентифицировать передаваемое действие. В таких случаях создают строку-идентификатор действия по правилам именования пакетов Java. Например, для обнаружения кота в большом здании:

companion object {
    val NEW_CAT_DETECTED = "ru.alexanderklimov.action.NEW_CAT"
}

Далее вы создаёте объект Intent, загружаете в него нужную информацию и вызываете метод sendBroadcast(), передав ему в качестве параметра созданный объект Intent. Дополнительные данные можно использовать в extras как необязательные параметры.

Виртуальный код для обнаружения кота:


val intent = Intent(NEW_CAT_DETECTED)
intent.putExtra("catname", CatName)
intent.putExtra("longitude", currentLongitude)
intent.putExtra("latitude", currentLatitude)
sendBroadcast(intent)

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

Существуют также родственные методы sendStickyBroadcast() и sendOrderedBroadcast().

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

Создайте новый проект и разместите на экране кнопку с надписью "Отправить сообщение".

В классе активности создаём уникальную строку и реализуем метод для щелчка кнопки. Также добавим дополнительные данные - первую часть послания радистки.


// До функции onCreate()
companion object {
    val WHERE_MY_CAT_ACTION: String = "ru.alexanderklimov.action.CAT"
    val ALARM_MESSAGE: String = "Срочно пришлите кота!"
}

// Код для щелчка кнопки
button.setOnClickListener {
    val intent = Intent()
    intent.action = WHERE_MY_CAT_ACTION
    intent.putExtra("ru.alexanderklimov.broadcast.Message", ALARM_MESSAGE)
    intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
    sendBroadcast(intent)
}

Запустив пример, вы можете нажать на кнопку и отправлять сообщение. Только ваше сообщение уйдёт в никуда, так как ни одно приложение не оборудовано приёмником для него. Исправим ситуацию и создадим приёмник в своём приложении. Мы будем сами принимать свои же сообщения.

Приёмник представляет собой обычный Java-класс на основе BroadcastReceiver. Вы можете создать вручную класс и наполнить его необходимыми методами. Раньше так и поступали. Но в студии есть готовый шаблон, который поможет немного сэкономить время.

Щёлкаем правой кнопкой мыши на названии пакета и выбираем New | Other | Broadcast Receiver

Create Broadcast Receiver

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

New Broadcast Receiver

Студия создаст изменения в двух местах. Во-первых, будет создан класс MessageReceiver:


package ru.alexanderklimov.broadcast

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent

class MessageReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
        TODO("MessageReceiver.onReceive() is not implemented")
    }
}

Во-вторых, в манифесте будет добавлен новый блок.


<receiver
    android:name=".MessageReceiver"
    android:enabled="true"
    android:exported="true" >
</receiver>

В него следует добавить фильтр, по которому он будет ловить сообщения.


<receiver
    android:name=".MessageReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="ru.alexanderklimov.action.CAT" />
    </intent-filter>
</receiver>

Вернёмся в класс приёмника и модифицируем метод onReceive().


override fun onReceive(context: Context, intent: Intent) {
    Toast.makeText(context, "Обнаружено сообщение: " +
            intent.getStringExtra("ru.alexanderklimov.broadcast.Message"),
        Toast.LENGTH_LONG).show()
}

Снова запустим пример и ещё раз отправим сообщение. Так как наше приложение теперь оборудовано не только передатчиком, но и приёмником, то оно должно уловить сообщение и показать его нам.

Send Broadcast

Вы можете создать другое приложение с приёмником, чтобы одно приложение посылало сообщение, а другое принимало.

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

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

  • Низкий заряд батареи
  • Нажатие на кнопку камеры
  • Установка нового приложения

Приёмник можно создать двумя способами - через манифест (мы использовали этот способ в примере) и программно через метод registerReceiver(). Между двумя способами есть существенная разница. Приёмник, заданный в манифесте, известен системе, которая сканирует файлы манифеста всех установленных приложений. Поэтому, даже если ваше приложение не запущено, оно всё равно сможет отреагировать на поступающее сообщение.

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

При программной регистрации приёмника мы можем также снять регистрацию, когда больше не нуждаемся в нём с помощью метода unregisterBroadcastReceiver().

Периодическое срабатывание каждую минуту

Рассмотрим пример периодического срабатывания приёмника каждую минуту с помощью системного намерения android.intent.action.TIME_TICK. Приёмник будет создан программно. Добавим на экран активности кнопку для регистрации широковещательного сообщения. Отмену регистрации сделаем в функции onDestroy(). Код макета произвольный.

Создадим вручную новый класс TimeBroadcastReceiver, наследующий от BroadcastReceiver:


package ru.alexanderklimov.broadcast

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
import java.text.Format
import java.text.SimpleDateFormat
import java.util.Date


class TimeBroadcastReceiver: BroadcastReceiver() {

    override fun onReceive(context: Context?, intent: Intent?) {
        val msgStr = StringBuilder("Текущее время: ")
        val formatter: Format = SimpleDateFormat("hh:mm:ss a")
        msgStr.append(formatter.format(Date()))
        Toast.makeText(context, msgStr, Toast.LENGTH_SHORT).show()
    }
}

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

Откроем код главной активности и зарегистрируем (а также снимем регистрацию) приёмник:


package ru.alexanderklimov.broadcast

import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity


class MainActivity : AppCompatActivity() {
    private val timeBroadcastReceiver = TimeBroadcastReceiver()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        val button: Button = findViewById(R.id.button)

        button.setOnClickListener {
            registerReceiver(
                timeBroadcastReceiver, IntentFilter(
                    "android.intent.action.TIME_TICK"
                )
            )
            Toast.makeText(
                getApplicationContext(), "Приёмник включен",
                Toast.LENGTH_SHORT
            ).show();
        }
    }

    override fun onDestroy() {
        super.onDestroy()

        unregisterReceiver(timeBroadcastReceiver)
        Toast.makeText(
            getApplicationContext(),
            "Приёмник выключён", Toast.LENGTH_SHORT
        )
            .show();
    }
}

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

Это один из примеров, когда приёмник следует регистрировать программно. Я видел часто на форумах вопросы, почему не работает данное намерение android.intent.action.TIME_TICK. А не надо было его регистрировать в манифесте.

В нашем примере мы устанавливали и снимали регистрацию через нажатия кнопок. Обычно включают регистрацию в методе onResume(), а снимают регистрацию в методе onPause().

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

Другой вариант

Напишем другой вариант. Разместите на экране TextView, в котором будет отображаться время. Код для активности ниже. Обратите внимание, что мы не создаём отдельный класс для BroadcastReceiver, включаем регистрацию в onResume() и снимаем регистрацию в onPause().


package ru.alexanderklimov.broadcast

import ...

class MainActivity : AppCompatActivity() {

    private val tickReceiver by lazy { makeBroadcastReceiver() }

    companion object {
        private fun getCurrentTimeStamp(): String {
            val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US)
            val now = Date()
            return simpleDateFormat.format(now)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)
        ...
    }

    override fun onResume() {
        super.onResume()

        dateTimeTextView.text = getCurrentTimeStamp()
        registerReceiver(tickReceiver, IntentFilter(Intent.ACTION_TIME_TICK))
    }

    override fun onPause() {
        super.onPause()

        try {
            unregisterReceiver(tickReceiver)
        } catch (e: IllegalArgumentException) {
            Log.e("Broadcast", "Time tick Receiver not registered", e)
        }
    }


    private fun makeBroadcastReceiver(): BroadcastReceiver {
        return object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent?) {
                if (intent?.action == Intent.ACTION_TIME_TICK) {
                    dateTimeTextView.text = getCurrentTimeStamp()
                }
            }
        }
    }
}

Далее идут старые примеры для Java. Попробуйте самостоятельно переделать их под Kotlin.

Автостарт Activity или Service при загрузке (перезагрузке) устройства

Ещё один полезный пример, который часто используется приложениями.

Если ваше приложение (сервис) должно запускаться сразу после перезагрузки устройства, то используйте намерение android.intent.action.BOOT_COMPLETED:


public class BootReceiver extends BroadcastReceiver {
    Context mContext;
    private final String BOOT_ACTION = "android.intent.action.BOOT_COMPLETED";

    @Override
    public void onReceive(Context context, Intent intent) {
        mContext = context;
        String action = intent.getAction();
        if (action.equalsIgnoreCase(BOOT_ACTION)) {
            // здесь ваш код
            // например, запускаем уведомление
            Intent intent = new Intent(context, ru.alexanderklimov.NotifyService.NotifyService.class);
            context.startService(intent);
            // в общем виде
            //для Activity 
            Intent activivtyIntent = new Intent(context, MyActivity.class);  
            activivtyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(activivtyIntent);  
              
            //для Service
            Intent serviceIntent = new Intent(context, MyService.class);
            context.startService(serviceIntent);
        }
    }
}

Мы создали отдельный класс для широковещательного сообщения. Также нужно создать разрешение и зарегистрировать приёмник в манифесте.


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

<receiver android:enabled="true" android:name=".BootReceiver"
        android:permission="android.permission.RECEIVE_BOOT_COMPLETED">

        <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
</receiver>

Смотри также Автозапуск приложения при загрузке

Следим за питанием

Нет, речь пойдёт не о правильном питании кота. Имеется в виду питание от электричества. Если ваше устройство отключить от зарядки, то система оповещает об этом событии через широковещательное намерение android.intent.action.ACTION_POWER_DISCONNECTED.

Не станем заводить новый приёмник, а откроем манифест и добавим дополнительный фильтр к приёмнику сообщений от радистки Кэт.


<receiver
    android:name=".MessageReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="ru.alexanderklimov.action.CAT" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"></action>
    </intent-filter>

</receiver>

А в классе MessageReceiver добавим код для метода.


@Override
public void onReceive(Context context, Intent intent) {
    //Toast.makeText(context, "Обнаружено сообщение: " +
    //                intent.getStringExtra("ru.alexanderklimov.broadcast.Message"),
    //        Toast.LENGTH_LONG).show();

    if (intent.getAction().equalsIgnoreCase("android.intent.action.ACTION_POWER_DISCONNECTED")) {
        String message = "Обнаружено сообщение "
                + intent.getAction();

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

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

Дополнительные материалы

Теория

Получаем показания батареи в реальном времени

Отслеживание состояния соединения Wi-Fi

DownloadManager

Секретный код

Реклама