В этой статье вы узнаете, как создать будильник, построенный на системном планировщике задач AlarmManager Андроида в FireMonkey. AlarmManager позволяет поставить на выполнение задачу, которую требуется выполнить в определенный момент времени. Главным отличием от обычного таймера является то, что задание будет выполнено не зависимо от того, живо ли ваше приложение или нет.
Система автоматически по наступлению указанного времени, выполнит ваш код. О том, как это работает и как этим пользоваться будет описано в этой статье.
Код проекта: Alarm Application Demo (XE 10)
Содержание
Описание проблемы
Представьте, что вам требуется сделать приложение будильник, которое должно в определенное время подать звуковой сигнал и разбудить пользователя. Мы могли бы использовать компонент TTimer для этого и по наступлению определенного времени подать звуковой сигнал:
procedure TFormAlarm.Timer1Timer(Sender: TObject); begin if Now = AlarmTime then PlayAlarmSound; end;
Но, как только приложение будет закрыто, ваш будильник не сработает. Такая реализация требует, чтобы приложение постоянно находилось в памяти и постоянно работало. Как вы понимаете в реальности на мобильных устройствах такой подход не допустим и приложение может быть, как выгружено системой в целях экономии ресурсов, так и приостановлено. Я уж не говорю о практичности такого будильника, который запрещает вам работать с другими приложениями на устройстве.
Что нам требуется, чтобы будильник сработал даже, когда приложение закрыто?
Нам нужно, чтобы кто-нибудь подал нам сигнал, когда придет время просыпаться. По этому сигналу мы могли бы запустить наше приложение и выполнить наш код. Например, проиграть звонок. Оказывается, Андроид уже предусмотрел для этих целей специальных механизм. Он называется AlarmManager.
Решение в виде AlarmManager
AlarmManager — это специальный системный сервис, позволяющий выполнить пользовательский код в определенный момент времени. Этот менеджер является частью системы Андроид, постоянно находится в памяти и бдит за временем и задачами. Как только приходит положенное время, он извлекает помещенную в него заранее задачу и инициирует выполнение задачи.
Типичный алгоритм работы с сервисом такой:
- Создаем класс «задачи». Наследник от BroadcastReceiver (об этом в этой статье позже), который предоставляет специальный абстрактный метод OnReceive, который будет вызван сервисом AlarmManager в указанный момент времени. В метод OnReceive через параметр сервис AlarmManager передаст наше заранее заготовленное сообщение с нашими параметрами.
- Регистрируем наш ресивер в нашем приложении. Говорим системе, что у нас есть такой ресивер.
- Формируете сообщение с нашими параметрами. Сообщение — это отложенное намерение PendingIntent (далее в статье).
- Запрашиваете у системы сервис AlarmManager
- Отправляете задачу, указывая: время/интервал выполнения задачи, отложенный интент (2 пункт).
Что такое интент?
Интент (Intent) — это дословно намерение на выполнение какого-то действия. По сути, интент представляет собой аналог сообщения или посылки в реальной жизни. Сообщение, имеющее:
- Адрес получателя — если получатель один.
- Категорию адресатов — если получателей несколько
- Набор данных, который мы хотим послать адресатам.
Интенты бывают двух видов:
- Intent — Мгновенные, которые отправляются сразу и выполняются сразу
- PendingIntent — Отложенные, которые складываются в очереди и отправляются уже в будущем.
В этой статье нас интересуют только отложенные уведомления. Так как мы хотим разбудить человека в определенный момент времени в будущем, а не прямо сейчас.
Шаг 0. Пара слов об использовании java в нативных приложения Delphi
Перед тем, как мы рассмотрим все шаги подробнее. Я хочу обратить ваше внимание на понимание процесса, как используются любые java классы в Delphi. Именно понимание этого, облегчит вам процесс интеграции ваших java классов в ваше нативное приложение.
Ваше Delphi приложение с точки зрения исполняемого кода состоит из двух главных частей:
- Нативная so библиотека с вашим кодом на языке Delphi
- Исполняемый код вашего приложения — classes.dex. Именно этот файл содержит стартовое активити вашего приложения. И именно этот файл загружает нативную so библиотеку с вашим кодом. Именно этот файл содержит java реализацию дополнительных классов, требующих для работы FireMonkey.
Если вы хотите добавить ваш собственный java класс, его нужно добавить в classes.dex файл. Это делается при помощи специальных утилит идущих в поставке с Android SDK.
Dex файл — это файл с инструкциями для исполняемой java машины Dalwik. Именно он и запускается на андроиде и выполняется на исполняемой машине Dalwik. Поскольку нам не доступны исходники из которых этот файл был построен, мы можем только добавить в этот файл свои классы.
Общий алгоритм добавления своих java классов такой:
- Создаем файлы с java классами.
- Компилируем java классы java компилятором javac и получаем class файлы
- Пакуем ваши class файлы в jar файл (архив со специальной внутренней структурой, манифестом и тд)
- Получаем dex файл из jar
- Смешиваем полученный dex файл c dex файлом Embarcadero. (Результирующий файл содержит старые классы и ваши новые.)
- Заменяем dex файл embarcadero новым через Deployment Manager.
Теперь ваши классы будут в вашем приложении. Остается только понять, как их вызывать и использовать из нативного кода. Для этого нужно получить «хедера» для них при помощи утилиты java2op, которая идет в поставке RAD Studio. Натравив ее на ваш jar файл, утилита выдаст pas файл с классами для работы с вашими java классами.
Теперь перейдем непосредственно к созданию BroadcastReceiver.
Шаг 1. Создание BroadcastReceiver
BroadcastReceiver — это абстрактный java, класс выступающий в качестве получателя интентов. Другими словами — он является тем получателем посылок, которые мы будем ему отправлять. Именно здесь мы и будем будить пользователя звонком будильника.
Теперь нам требуется сделать наследника этого класса и написать код воспроизведения мелодии. Сделать в Delphi мы это не можем, так как JNI не позволяет наследовать java классы в нативном коде.
JNI — это специальная библиотека Java виртуальной машины для использования java их нативных языков C++, Delphi и тд.
Поэтому используя java создаем нашего наследника com.test.AlarmReceiver. Создаем в обычном текстовом редакторе текстовый файл AlarmReceiver.java, перекрываем метод OnReceive и выполняем проигрывание стандартной мелодии будильника. Для этого используем RingtoneManager.
package com.test; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; public class AlarmReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); Ringtone ringtone = RingtoneManager.getRingtone(context.getApplicationContext(), notification); ringtone.play(); } }
Этот файл обязательно помещаем в каталоги: «com\test«. Так как пакет этого класса назван com.test.
Теперь необходимо этот файл скомпилировать и добавить в наше приложение, чтобы мы могли его использовать (смотрите шаг 0). Наверное, это наиболее сложная часть — внедрение вашего класса в уже готовый файл от Embarcadero.
Воспользуемся bat файлом для сборки файла (я позаимствовал его у Андрея Ефимов). Я взял за основу батник и немного модифицировал его.
Вам требуется указать правильные пути в своем окружении:
- ANDROID — путь к sdk адроида
- ANDROID_PLATFORM — путь к версии андроида.
- DX_LIB — путь к инструментам для получения и соединения dx файлов
- EMBO_DEX — путь к местоположению эмбаркадеровского classes.dex файлы
@echo off setlocal REM Здесь указываем каталог к Android SDK if x%ANDROID% == x set ANDROID=C:\Users\Public\Documents\Embarcadero\Studio\14.0\PlatformSDKs\adt-bundle-windows-x86-20131030\sdk REM Здесь указываем версию Android платформы set ANDROID_PLATFORM=%ANDROID%\platforms\android-19 set DX_LIB=%ANDROID%\build-tools\19.0.1\lib set EMBO_DEX="C:\Program Files\Embarcadero\Studio\14.0\lib\android\debug\classes.dex" set PROJ_DIR=%CD% set VERBOSE=0 echo ========================================================================================== echo. echo 1. Compiling the Java service activity source files echo. mkdir output 2> nul mkdir output\classes 2> nul if x%VERBOSE% == x1 SET VERBOSE_FLAG=-verbose javac %VERBOSE_FLAG% -Xlint:deprecation -cp %ANDROID_PLATFORM%\android.jar -d "output\classes" "src\com\test\AlarmReceiver.java" echo ========================================================================================== echo. echo 2. Creating jar containing the new classes echo. mkdir output\jar 2> nul if x%VERBOSE% == x1 SET VERBOSE_FLAG=v jar c%VERBOSE_FLAG%f %PROJ_DIR%\output\jar\test_classes.jar -C output\classes com\test\AlarmReceiver.class echo ========================================================================================== echo. echo 3. Converting from jar to dex... echo. mkdir output\dex 2> nul if x%VERBOSE% == x1 SET VERBOSE_FLAG=--verbose call %DX_LIB%\dx.jar --dex %VERBOSE_FLAG% --output=output\dex\test_classes.dex output\jar\test_classes.jar echo ========================================================================================== echo. echo Merging dex files echo.com.android.dx.merge.DexMerger java -cp %DX_LIB%\dx.jar com.android.dx.merge.DexMerger %PROJ_DIR%\output\dex\classes.dex %PROJ_DIR%\output\dex\test_classes.dex %EMBO_DEX% echo Tidying up echo. rmdir /s /q output\classes del output\dex\test_classes.dex rmdir /s /q output\jar echo ========================================================================================== echo Now we have the end result, which is output\dex\classes.dex :Exit endlocal
После того, как вы вызовите эту утилиту, в папке «output\dex» будет находиться новый classes.dex файл. Теперь нужно заменить им classes.dex файл по умолчанию. Для этого открываем Deployment в RAD Studio (Menu -> Project -> Deployment) и добавляем новый файл. Важно, что путь назначения (Remote Path) для этого файла должен быть «classes\«.
Теперь, нужно получить delphi хедеры для нашего AlertReceiver java класса. Для этого пользуемся утилитой java2Op (утилита располагается в папке bin или bin\converterers\java2op в каталоге с Rad Studio):
java2op -jar output\jar\test_classes.jar -unit Androidapi.JNI.AlarmReceiver
на выходе получаем хедер:
unit Androidapi.JNI.AlarmReceiver; interface uses Androidapi.JNIBridge, Androidapi.JNI.JavaTypes; type // ===== Forward declarations ===== JAlarmReceiver = interface;//com.test.AlarmReceiver //JStringBuffer = interface;//java.lang.StringBuffer //JStringBuilder = interface;//java.lang.StringBuilder // ===== Interface declarations ===== JAlarmReceiverClass = interface(JObjectClass) ['{D8B178E9-529D-4E45-B919-F4BAC1EA6C63}'] {class} function init: JAlarmReceiver; cdecl; end; [JavaSignature('com/test/AlarmReceiver')] JAlarmReceiver = interface(JObject) ['{8EEC0BB0-FDAF-486E-8F55-140B2E3F971C}'] end; TJAlarmReceiver = class(TJavaGenericImport<JAlarmReceiverClass, JAlarmReceiver>) end; // java.lang.StringBuffer // java.lang.StringBuilder implementation procedure RegisterTypes; begin TRegTypes.RegisterType('Androidapi.JNI.AlarmReceiver.JAlarmReceiver', TypeInfo(Androidapi.JNI.AlarmReceiver.JAlarmReceiver)); //TRegTypes.RegisterType('Androidapi.JNI.AlarmReceiver.JStringBuffer', TypeInfo(Androidapi.JNI.AlarmReceiver.JStringBuffer)); //TRegTypes.RegisterType('Androidapi.JNI.AlarmReceiver.JStringBuilder', TypeInfo(Androidapi.JNI.AlarmReceiver.JStringBuilder)); end; initialization RegisterTypes; end.
Шаг 2. Регистрация AlarmReceiver
Процесс регистрации прост. Нужно всего лишь добавить в манифест приложения эту информацию. Открываем в текстовом редакторе файл AndroidManifest.template.xml, расположенный в папке с исходниками вашего приложения. И добавляем строчку
<receiver android:name="com.test.AlarmReceiver" />
После <%receivers%>. Таким образом мы сказали операционной системе, что у нас есть ресивер AlarmManager и он находится в пакете com.test.
Шаг 3. Формирование интента
Интент формируется просто:
function CreateAlarmIntent(const AID: Integer): JPendingIntent; var Intent: JIntent; begin Intent := TJIntent.Create; Intent.setClass(TAndroidHelper.Context, TJlang_Class.Wrap(TJAlarmReceiver.GetClsID)); Result := TJPendingIntent.JavaClass.getBroadcast(TAndroidHelper.Context, AID, Intent, TJPendingIntent.JavaClass.FLAG_UPDATE_CURRENT); end;
В 5 строчке делаем сообщение, и в 6 строчке указываем, что класс, который должен принять этот сообщение — это наш AlarmReceiver. В положенное время:
- Сервис системы посмотрит на сообщение
- Извлечет название класса
- Создаст класс
- Передаст туда это сообщение в метод OnReceive
В 7 строчке мы создаем отложенное сообщение. Указывая контекст — приложения в рамках которого будет наше сообщение. AID — уникальный ID для возможности отменить задание в будущем. FLAG_UPDATE_CURRENT — указывает, что задание нужно обновить, если такое уже было в системе.
Шаг 4/5. Запрашиваем сервис AlarmManager и отправляем нашу задачу
Создаем интент и посылаем его через метод set.
procedure TForm19.Button1Click(Sender: TObject); var PendingIntent: JPendingIntent; begin PendingIntent := CreateAlarmIntent(1); AndroidHelper.AlarmManager.&set(TJAlarmManager.JavaClass.RTC_WAKEUP, DateTimeLocalToUnixMSecGMT(Now + EncodeTime(0, 0, 10, 0)), PendingIntent); end;
- RTC_WAKEUP — указывает, что когда время выполнения задания придет, нужно разбудить устройство, если оно находится в спящем состоянии.
- Второй параметр — время, когда требуется выполнить задание.
Код метода конвертирующего TDateTime во время андроида.
function DateTimeLocalToUnixMSecGMT(const ADateTime: TDateTime): Int64; begin Result := DateTimeToUnix(ADateTime) * MSecsPerSec - Round(TTimeZone.Local.UtcOffset.TotalMilliseconds); end;
Теперь можно запустить приложение. Выберите время, когда вы хотите проснуться и и нажмите кнопку поставить будильник.
Код проекта: Alarm Application Demo (XE 10)