Два года назад я увлекся мобильной разработкой под Android. Тогда я писал простенькие приложения для парсинга веб-сайтов. Программный код писался на Java. Это очень мощный язык, но для написания простых легковесных приложений, не выполняющих сложных задач, его объектно-ориентированная парадигма показалась мне не слишком кстати. В то время я только начинал знакомиться с JavaScript. Изначально он привлек меня своей простотой, затем я стал открывать в нем все большие и большие возможности. Я был знаком с HTML5 и CSS3, удовольствия ради создавал симпатичные веб-страницы.


Однажды я узнал об Apache Cordova — фреймворке, который позволяет писать мобильные приложения на HTML, CSS и JavaScript. Я сразу же установил его.


Работать с Cordova оказалась по-настоящему удобно. Тем не менее, судя по количеству тематических статей в интернете, фреймворк пока не получил достаточно широкого распространения. Хотя на нем реализовано, например, мобильное приложение Википедии.


Я считаю, что это связано с недостаточным функционалом стандартного Cordova API. Для работы с Bluetooth, распознаванием и синтезом речи, камерой и т.д. требуется использовать плагины. Плагины могут писаться для одной или нескольких платформ на их нативном языке.


Для нужд разработчиков были написаны тысячи плагинов, которые находятся в открытом доступе. Я был доволен до того момента, когда мне потребовалось создать приложение, которое бы даже после своего закрытия отправляло пользователю push-уведомления каждый раз через определенный промежуток времени. На Android такой фукнционал реализуется через фоновый процесс. Плагина для создания чего-либо подобного не нашлось. Плагин https://github.com/katzer/cordova-plugin-background-mode позволял запускать фоновый процесс, который останавливался сразу же, как закрывалось приложение.


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


О пути создания приложения и плагина я хочу рассказать в этом посте.


Установка Cordova и запуск приложения


Перед установкой Cordova обязательно установите Android SDK, Apache Ant и Java.


Затем добавьте их в системный путь:


На Windows: введите в поиск "Панель управления". Нажмите на Дополнительные параметры системы. Нажмите Переменные среды. В разделе Переменные среды выберите переменную среды PATH. Нажмите Изменить.
Добавьте в конец строки: ;C:\Development\adt-bundle\sdk\platform-tools;C:\Development\adt-bundle\sdk\tools;%JAVA_HOME%\bin;%ANT_HOME%\bin.


На Mac/Linux: откройте .bash_profile командой open ~/.bash_profile. Добавьте туда системные переменные: export PATH=${PATH}:/Development/adt-bundle/sdk/platform-tools:/Development/adt-bundle/sdk/tools. Добавьте пути для Java и Apache Ant, если их нет.


Обязательно укажите СВОИ пути к папкам.


  1. С сайта https://nodejs.org/en/download/ устанавливаем Node.js, позволяющий транслировать JavaScript в нативные коды. Платформа содержит пакетный менеджер npm, с помощью которого мы установим Cordova.


  2. Открываем командную строку или терминал и устанавливаем Cordova
    npm install -g cordova


  3. Создаем новый проект:

cordova create MyApp com.app.myapp MyApp


Первый аргумент — название папки, в которой будут храниться коды и файлы приложения. В данном случае это MyPluginApp.
Второй аргумент — название пакета приложения, его уникальный идентификатор.
Третий аргумент — название приложения, которое будет видно пользователю. Как правило, совпадает с названием папки.


Не забудьте перейти в папку приложения:


cd MyApp.


  1. Добавим платформы, на которых будет запускаться приложение. В моем случае это Android.

cordova platform add android


  1. Теперь запустим свое первое Cordova-приложение%

cordova run android


image


Пишем плагин


Теперь создадим плагин, который будет проводить API к классу Service, отвечающему за фоновый процесс.


Для написания плагина требуется создать обычную папку. Создадим ее в той же директории, в которой находится папка приложения. Назовем ее MyPlugin.


Любой плагин содержит файл plugin.xml. Создадим и добавим его в папку плагина.


plugin.xml:


<?xml version="1.0" encoding="utf-8"?>
<plugin xmlns="http://www.phonegap.com/ns/plugins/1.0"
        id="com.example.myplugin"
        version="1.0">

  <name>MyPlugin</name>

  <engines>
    <engine name="cordova" version=">=3.4.0"/>
  </engines>

  <asset src="www/MyPlugin.js" target="js/MyPlugin.js"/>

  <js-module src="www/MyPlugin.js" name="MyPlugin">
    <clobbers target="MyPlugin" /> <!-- объект, к которому обращается приложение -->
  </js-module>

  <platform name="android">

    <config-file target="res/xml/config.xml" parent="/*">
      <feature name="MyPlugin">
        <param name="android-package" value="com.example.plugin.MyPlugin"/> <!-- регистрируем главный java-класс -->
      </feature>
    </config-file>

    <config-file target="AndroidManifest.xml" parent="/manifest/application">
      <service android:name="com.example.plugin.MyService" /> <!-- регистрируем фоновый процесс (сервис) -->
    </config-file>

    <framework src="com.android.support:support-v4:+" /> <!-- библиотека, содержащая необходимые java-классы  -->
    <!-- основной java-файл --><source-file src="src/android/MyPlugin.java" target-dir="src/com/example/plugin/"/>
    <!-- java-файл, содержащий Service --><source-file src="src/android/MyService.java" target-dir="src/com/example/plugin/"/>
  </platform>

</plugin>

В этом файле хранится информация о плагине, необходимых для его функционирования файлах и библиотеках, а также особенности API. Как вы можете заметить, мы ссылаемся на один JavaScript- и два Java-файла. Так создадим же их!


В папку MyPlugin добавим папку www и там создадим файл MyPlugin.js. Там мы опишем функции, с помощью которых приложение сможет взаимодействовать с нативным кодом. То есть этот файл будет служить своеобразным проводником между JavaScript-кодом разработчика приложения и Java-кодом разработчика плагина.


MyPlugin.js:


module.exports = {
    runBackground: function (successCallback, errorCallback) {
        cordova.exec(successCallback, errorCallback, "MyPlugin", "runBackground", [])
    }
}

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


И наконец приступим к работе с нативным кодом. Создаем папку src/android внутри MyPlugin. Здесь будет находиться два Java-класса: MyPlugin.java и MyService.java.


MyPlugin.java:


package com.example.plugin;

import org.apache.cordova.*;
import org.json.JSONArray;
import org.json.JSONException;
import android.widget.Toast;
import android.content.Context;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;

public class MyPlugin extends CordovaPlugin {

    @Override
    public boolean execute(String action, JSONArray data, CallbackContext callbackContext) throws JSONException {
        if (action.equals("runBackground")) {
            Context context = cordova.getActivity().getApplicationContext();
            Intent service = new Intent(context, MyService.class);
            context.startService(service);
            return true;
        } else {
            return false;
        }
    }

}

Сначала импортируются библиотеки, в том числе org.apache.cordova.*, которая позволяет взаимодействовать с приложением на базе Cordova. Далее мы можем встретить Notification и NotificationManager — библиотеки для работы с уведомлениями. Ради библиотеки NotificationCompat мы и добавили в plugin.xml строку <framework src="com.android.support:support-v4:+" />.


Как только приложение вызывает какую-либо функцию плагина, выполняется метод execute, которому передается строка action. В зависимости от значения этой строки выполняются различные наборы действий. В нашем плагине action может принимать лишь одно значение — runBackground. Создается и вызывается объект нашего фонового процесса.


MyService.java:


package com.example.plugin;

import org.apache.cordova.*;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.content.Context;
import android.os.Handler;
import android.support.v4.app.NotificationCompat;

public class MyService extends Service {

    Handler mHandler = new Handler();

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        mHandler.postDelayed(ToastRunnable, 5000);

        return START_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    Runnable ToastRunnable = new Runnable() {
        public void run() {
            Context context = getApplicationContext();

            NotificationManager mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

            NotificationCompat.Builder mBuilder =
            new NotificationCompat.Builder(context)
                .setSmallIcon(context.getApplicationInfo().icon)
                .setWhen(System.currentTimeMillis())
                .setContentTitle("It works!")
                .setTicker("Ticker")
                .setContentText("Text")
                .setNumber(1)
                .setAutoCancel(true);

            mNotificationManager.notify("App Name", 228, mBuilder.build());

            mHandler.postDelayed( ToastRunnable, 5000);
        }
    };

}

Этот класс — обычный Service. При запуске сервиса его метод onStartCommand возвращает START_STICKY. Благодаря этому даже после закрытия приложения процесс продолжает жить. Чтобы отправлять push-уведомления каждые 5 секунд, был создан поток Runnable, который вызывается хэндлером каждые 5 секунд. В теле потока происходит создание и отправка уведомления.


Добавляем плагин в приложение


Переходим в директорию приложения и добавляем наш плагин:


cordova plugin add ../MyPlugin.


Затем переходим в www/js/index.js и переписываем метод onDeviceReady следующим образом:


onDeviceReady: function() {
       app.receivedEvent('deviceready');

        var failure = function() {
            alert("Error calling MyPlugin");
        }

        MyPlugin.runBackground(function() {}, failure);
}

Через объект MyPlugin вызывается функция runBackground, которой передаем функцию, которая вызывается в случае ошибки и пустую функцию, если все прошло успешно. Эта функция была оставлена пустой, так как подтверждением успешного вызова у нас уже является отправка push-уведомления.


Заключение


Теперь у нас есть работающее приложение, которое отправляет каждые 5 секунд push-уведомления даже после того, как мы его закроем.


image


image


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


Исходные коды приложения и плагина вы можете загрузить по ссылкам: само приложение, плагин.

Комментарии (13)


  1. dolphin4ik
    04.05.2016 14:37

    Годно!


  1. deenween
    04.05.2016 15:10

    а как можно увеличить скорость отклика приложения?


    1. DewDif
      04.05.2016 15:12

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


    1. maks_ohs
      04.05.2016 15:45
      +3

      Можно вместо click эвентов использовать touchend. Это уберёт задержку в 300ms.



  1. deenween
    04.05.2016 15:18

    ясно, спасибо.


  1. SerP1983
    04.05.2016 18:08
    -1

    А мне не понравилось:
    1) Оформление статьи. Скрины экрана телефона не помещаются на экран моего монитора.
    2) Период в 5 сек. Замерьте, пожалуйста, через какой промежуток времени ваше приложение сожрет всю батарею. Даже если это просто пример, не мешало бы написать, что так делать совсем не стоит.
    3) Про «на форумах утверждали, что создать полноценный фоновый процесс на Cordova невозможно» совершенно правильно написано. То что вы добавили «START_STICKY», еще ничего не значит, так как это работает не всегда. Так как вы и так выводите notification, надо было делать foreground службу. Почитайте эту статью habrahabr.ru/post/265159

    Резюмирую: пока плохо.


    1. DewDif
      04.05.2016 18:29
      -1

      Отвечу по тому же плану:
      1) Скрины не умещаются по высоте. Действительно недочет, но изображение можно открыть в новой вкладке. Больше такого допускать не буду.
      2) Разумеется, каждые 5 секунд отправлять push-notification не нужно. Столь короткий промежуток я сделал для отладки. Если бы я поставил минуту, то мне бы пришлось ждать целых 60 секунд после закрытия приложения для того, чтобы убедиться, что сервис работает даже после закрытия приложения.
      3) Под полноценным фоновым процессом я подразумеваю не тот, который, подобно вирусу, не позволяет себя завершить, а тот, который работает даже после закрытия приложения и завершения всех Activity. Рекомендованную Вами статью прочитал, push-уведомления действительно удобно отправлять через startForeground(). Но foreground работает лишь с уведомлениями, а я хотел показать, что возможно создать фоновый процесс, который может выполнять и другие задачи. В таком случае лучше подходит просто липкий Service.

      Ваши замечания я принял во внимание.


      1. SerP1983
        04.05.2016 23:32
        +2

        Вы, видимо, невнимательно прочитали статью. На KitKat ваш пример не будет работать ожидаемо. А именно, «после закрытия приложения и завершения всех Activity» служба работать не будет. И, пожалуйста, не путайте push-уведомления и notification. К чему я пишу это. Ваша статья может оказаться немного «вредной» для людей, кто просто решит взять ваш плагин, не разбираясь, что к чему. Неплохо было бы его доработать, выложить на гитхаб. Тогда бы этот плагин несомненно стал полезным и нужным.


        1. DewDif
          05.05.2016 15:27

          Учту)


  1. lega
    04.05.2016 20:35

    даже после того, как мы его закроем.

    А что будет если перезагрузить телефон, приложение будет автоматический загружено в фон?


    1. DewDif
      04.05.2016 21:33

      Чтобы сделать автозагрузку при старте, нужно поработать с AndroidManifest.xml. В Cordova это сделать вполне возможно. Используя широковещательный приемник, можно отследить событие запуска и запустить сервис. Здесь Вы можете найти пример


  1. Focushift
    05.05.2016 13:59
    +1

    Как мне кажется, проблема в том, что описывается написание плагина на нативном коде и все… а само приложение у нас работает на чем?
    С таким же успехом можно писать нативное приложение. Я понимаю что это пример «как прикрутить плагин», но без обратной связи с основным кодом приложения толку для меня лично 0.