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

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

Шкодим

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

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

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

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

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

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

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

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

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

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

public static final String NEW_CAT_DETECTED = "ru.alexanderklimov.action.NEW_CAT";

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

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


Intent intent = new Intent(NEW_CAT_DETECTED);
// Или так
// Intent intent = new Intent();
// intent.setAction(NEW_CAT_DETECTED);

intent.putExtra("catname", CatName);
intent.putExtra("longitude", currentLongitude);
intent.putExtra("latitude", currentLatitude);
sendBroadcast(intent);

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

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

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

Создайте новый проект и разместите на экране кнопку с надписью "Отправить сообщение". Присвойте атрибуту onClick название метода, в котором будет происходит отправка широковещательного сообщения.


<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Отправить сообщение"
    android:id="@+id/button"
    android:layout_gravity="center_horizontal"
    android:onClick="sendMessage" />

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


public static final String WHERE_MY_CAT_ACTION = "ru.alexanderklimov.action.CAT";
public static final String ALARM_MESSAGE = "Срочно пришлите кота!";

public void sendMessage(View view) {
    Intent intent = new Intent();
    intent.setAction(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.testapplication;

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

public class MessageReceiver extends BroadcastReceiver {
    public MessageReceiver() {
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        // TODO: This method is called when the BroadcastReceiver is receiving
        // an Intent broadcast.
        throw new UnsupportedOperationException("Not yet 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
public void 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. Приёмник будет создан программно. Добавим на экран активности две кнопки для регистрации и отмены регистрации широковещательного сообщения.


<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:onClick="registerBroadcastReceiver"
    android:text="Регистрация" />

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:onClick="unregisterBroadcastReceiver"
    android:text="Отмена" />

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


package ru.alexanderklimov.testapplication;

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;

public class TimeBroadcastReceiver extends BroadcastReceiver {
    public TimeBroadcastReceiver() {
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        StringBuilder msgStr = new StringBuilder("Текущее время: ");
        Format formatter = new SimpleDateFormat("hh:mm:ss a");
        msgStr.append(formatter.format(new Date()));
        Toast.makeText(context, msgStr, Toast.LENGTH_SHORT).show();
    }
}

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

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


package ru.alexanderklimov.testapplication;

import android.os.Bundle;
import android.app.Activity;
import android.content.IntentFilter;
import android.view.Menu;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends Activity {

    private TimeBroadcastReceiver mTimeBroadCastReceiver = new TimeBroadcastReceiver();

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

    // регистрируем широковещательный приёмник
    // для намерения "android.intent.action.TIME_TICK".
    // Данное намерение срабатывает каждую минуту
    public void registerBroadcastReceiver(View view) {
        this.registerReceiver(mTimeBroadCastReceiver, new IntentFilter(
                "android.intent.action.TIME_TICK"));
        Toast.makeText(getApplicationContext(), "Приёмник включен",
                Toast.LENGTH_SHORT).show();
    }

    // Отменяем регистрацию
    public void unregisterBroadcastReceiver(View view) {
        this.unregisterReceiver(mTimeBroadCastReceiver);

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

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

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

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

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

TIME_TICK на Kotlin

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


package ru.alexanderklimov.as36

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import java.text.SimpleDateFormat
import java.util.*

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()
                }
            }
        }
    }
}

Автостарт 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

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

Реклама