Руководство по разработке плагинов для Android
В этом разделе подробно описано, как реализовать код нативного плагина на платформе Android. Прежде чем приступать к чтению, ознакомьтесь с Руководством по разработке плагинов для общего обзора структуры плагина и его стандартного JavaScript-интерфейса. В этом разделе продолжается демонстрация образца плагина echo, который общается от Cordova webview до нативной платформы и обратно. Для другого примера также см. комментарии в CordovaPlugin.java.
Плагины для Android основаны на Cordova-Android, который построен на базе Android WebView с нативным мостом. Нативная часть плагина Android состоит как минимум из одного Java-класса, который расширяет класс CordovaPlugin и переопределяет один из его методов execute.
Сопоставление классов плагинов
Интерфейс плагина JavaScript использует метод cordova.exec следующим образом:
exec(<successFunction>, <failFunction>, <service>, <action>, [<args>]);
Это передаёт запрос из WebView на нативную сторону Android, фактически вызывая метод action в классе service, с дополнительными аргументами, переданными в массиве args.
Независимо от того, распространяете ли вы плагин как файл Java или как собственный файл jar, плагин должен быть указан в файле res/xml/config.xml вашего приложения Cordova-Android. Для получения дополнительной информации о том, как использовать файл plugin.xml для вставки этого элемента feature, см. раздел Плагины приложения:
<feature name="<service_name>">
<param name="android-package" value="<full_name_including_namespace>" />
</feature>
Имя службы соответствует имени, используемому в вызове JavaScript exec. Значение — это полное имя пространства имён Java-класса. В противном случае плагин может скомпилироваться, но останется недоступным для Cordova.
Инициализация и жизненный цикл плагина
Один экземпляр объекта плагина создаётся на всё время существования каждого WebView. Плагины не инициализируются до тех пор, пока они не будут впервые упомянуты в вызове из JavaScript, если не <param> с атрибутом onload name установлено в значение "true" в config.xml. Например,
<feature name="Echo">
<param name="android-package" value="<full_name_including_namespace>" />
<param name="onload" value="true" />
</feature>
Плагины должны использовать метод initialize для своей логики запуска.
@Override
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
super.initialize(cordova, webView);
// your init code here
}
Плагины также имеют доступ к событиям жизненного цикла Android и могут обрабатывать их, расширяя один из предоставленных методов (onResume, onDestroy, и т. д.). Плагины с длительными запросами, фоновой активностью, такой как воспроизведение медиа, обработчики событий или внутреннее состояние, должны реализовывать метод onReset(). Он выполняется, когда WebView переходит на новую страницу или обновляется, что перезагружает JavaScript.
Написание плагина Android Java
Вызов JavaScript инициирует запрос плагина на нативной стороне, а соответствующий Java-плагин правильно сопоставлен в файле config.xml, но как выглядит окончательный класс Java-плагина Android? Всё, что отправляется в плагин с помощью функции JavaScript exec, передаётся в метод класса плагина execute. Большинство реализаций execute выглядят так:
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if ("beep".equals(action)) {
this.beep(args.getLong(0));
callbackContext.success();
return true;
}
return false; // Returning false results in a "MethodNotFound" error.
}
Параметр exec функции JavaScript action соответствует частному методу класса для отправки с необязательными параметрами.
При обработке исключений и возвращении ошибок важно, чтобы ошибки, возвращаемые в JavaScript, насколько это возможно, соответствовали именам исключений Java.
Потоки
JavaScript-код плагина не выполняется в основном потоке интерфейса WebView; вместо этого он выполняется в потоке WebCore, как и метод execute. Если вам необходимо взаимодействовать с пользовательским интерфейсом, используйте метод Activity's runOnUiThread, как показано ниже:
@Override
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
if ("beep".equals(action)) {
final long duration = args.getLong(0);
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
...
callbackContext.success(); // Thread-safe.
}
});
return true;
}
return false;
}
Если вам не нужно выполнять код в потоке пользовательского интерфейса, но также не нужно блокировать поток WebCore, выполните свой код с помощью Cordova ExecutorService, полученного с помощью cordova.getThreadPool(), как показано ниже:
@Override
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
if ("beep".equals(action)) {
final long duration = args.getLong(0);
cordova.getThreadPool().execute(new Runnable() {
public void run() {
...
callbackContext.success(); // Thread-safe.
}
});
return true;
}
return false;
}
Добавление зависимых библиотек
Если ваш плагин Android имеет дополнительные зависимости, они должны быть перечислены в файле plugin.xml одним из двух способов.
Предпочтительный способ — использование тега <framework /> (см. Спецификацию плагина для получения более подробной информации). Указание библиотек таким образом позволяет разрешать их с помощью логики управления зависимостями Gradle . Это позволяет использовать такие широко используемые библиотеки, как gson, android-support-v4 и google-play-services, несколькими плагинами без конфликтов.
Второй вариант — использование тега <lib-file /> для указания местоположения файла jar (см. Спецификацию плагина для получения более подробной информации). Этот подход следует использовать только в том случае, если вы уверены, что ни один другой плагин не будет зависеть от библиотеки, на которую вы ссылаетесь (например, если библиотека специфична для вашего плагина). В противном случае вы рискуете вызвать ошибки сборки для пользователей вашего плагина, если другой плагин добавит ту же библиотеку. Стоит отметить, что разработчики приложений Cordova не обязательно являются разработчиками нативных приложений, поэтому ошибки сборки нативной платформы могут быть особенно раздражающими.
Пример плагина Android Echo
Чтобы соответствовать функции echo JavaScript-интерфейса, описанной в разделе Плагины приложения, используйте plugin.xml для вставки спецификации feature в файл config.xml локальной платформы:
<platform name="android">
<config-file target="config.xml" parent="/*">
<feature name="Echo">
<param name="android-package" value="org.apache.cordova.plugin.Echo"/>
</feature>
</config-file>
<source-file src="src/android/Echo.java" target-dir="src/org/apache/cordova/plugin" />
</platform>
Затем добавьте следующее в файл src/android/Echo.java:
package org.apache.cordova.plugin;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CallbackContext;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* This class echoes a string called from JavaScript.
*/
public class Echo extends CordovaPlugin {
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if (action.equals("echo")) {
String message = args.getString(0);
this.echo(message, callbackContext);
return true;
}
return false;
}
private void echo(String message, CallbackContext callbackContext) {
if (message != null && message.length() > 0) {
callbackContext.success(message);
} else {
callbackContext.error("Expected one non-empty string argument.");
}
}
}
Необходимые импорты в начале файла расширяют класс от CordovaPlugin, который переопределяет метод execute() для получения сообщений от exec(). Метод execute() сначала проверяет значение action, для которого в этом случае существует только одно допустимое значение echo. Любой другой параметр возвращает false и приводит к ошибке INVALID_ACTION, которая преобразуется в обратный вызов ошибки на стороне JavaScript.
Далее метод извлекает строку echo, используя метод args объекта getString, указав первый параметр, переданный в метод. После того, как значение передано в закрытый метод echo, оно проверяется на предмет параметров, чтобы убедиться, что оно не null или не пустая строка, в этом случае callbackContext.error() вызывает обратный вызов ошибки JavaScript. Если различные проверки пройдены, callbackContext.success() возвращает исходную строку message в обратный вызов успеха JavaScript в качестве параметра.
Интеграция с Android
Android имеет систему Intent, которая позволяет процессам общаться друг с другом. Плагины имеют доступ к объекту CordovaInterface, который может получить доступ к Android Activity, выполняющей приложение. Это контекст Context, необходимый для запуска нового Android Intent.
Объект CordovaInterface позволяет плагинам запускать Activity для получения результата и устанавливать плагин обратного вызова на случай, если Intent возвращается в приложение.
Начиная с Cordova 2.0, плагины больше не могут напрямую получать доступ к Context, и устаревшее член ctx устарело. Все методы ctx существуют в Context, поэтому как getContext(), так и getActivity() могут возвращать необходимый объект.
Разрешения Android
Разрешения Android до недавнего времени обрабатывались во время установки, а не во время выполнения. Эти разрешения должны быть объявлены в приложении, использующем разрешения, и эти разрешения должны быть добавлены в Android Manifest. Это можно сделать, используя config.xml для вставки этих разрешений в файл AndroidManifest.xml. В примере ниже используется разрешение Contacts.
<config-file target="AndroidManifest.xml" parent="/*">
<uses-permission android:name="android.permission.READ_CONTACTS" />
</config-file>
Разрешения во время выполнения (Cordova-Android 5.0.0+)
Android 6.0 «Marshmallow» ввёл новую модель разрешений, где пользователь может включать и отключать разрешения по мере необходимости. Это означает, что приложения должны обрабатывать эти изменения разрешений, чтобы быть готовыми к будущим версиям, что и было целью выпуска Cordova-Android 5.0.0.
Разрешения, которые необходимо обрабатывать во время выполнения, можно найти в документации Android Developer здесь.
Что касается плагина, то разрешение можно запросить, вызвав метод разрешения; его сигнатура приведена ниже:
cordova.requestPermission(CordovaPlugin plugin, int requestCode, String permission);
Для сокращения объёма кода принято назначать его локальной статической переменной:
public static final String READ = Manifest.permission.READ_CONTACTS;
Также принято определять requestCode следующим образом:
public static final int SEARCH_REQ_CODE = 0;
Затем в методе exec разрешение должно быть проверено:
if(cordova.hasPermission(READ))
{
search(executeArgs);
}
else
{
getReadPermission(SEARCH_REQ_CODE);
}
В этом случае мы просто вызываем requestPermission:
protected void getReadPermission(int requestCode)
{
cordova.requestPermission(this, requestCode, READ);
}
Это вызовет activity и отобразит запрос разрешения. После того, как пользователь предоставит разрешение, результат необходимо обработать с помощью метода onRequestPermissionResult, который должен переопределять каждый плагин. Пример этого приведён ниже:
public void onRequestPermissionResult(int requestCode, String[] permissions,
int[] grantResults) throws JSONException
{
for(int r:grantResults)
{
if(r == PackageManager.PERMISSION_DENIED)
{
this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR));
return;
}
}
switch(requestCode)
{
case SEARCH_REQ_CODE:
search(executeArgs);
break;
case SAVE_REQ_CODE:
save(executeArgs);
break;
case REMOVE_REQ_CODE:
remove(executeArgs);
break;
}
}
Приведённый выше оператор switch вернётся из запроса и, в зависимости от переданного requestCode, вызовет соответствующий метод. Следует отметить, что запросы разрешений могут накапливаться, если выполнение не обрабатывается должным образом, и этого следует избегать.
Помимо запроса разрешения на одно разрешение, также возможно запросить разрешения для всей группы, определив массив разрешений, как это сделано в плагине Geolocation:
String [] permissions = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION };
Затем при запросе разрешения нужно сделать следующее:
cordova.requestPermissions(this, 0, permissions);
Это запрашивает разрешения, указанные в массиве. Хорошей идеей является предоставление общедоступного массива разрешений, так как это могут использовать плагины, использующие ваш плагин в качестве зависимости, хотя это не обязательно.
END_OF_DOCUMENT_MARKERОтладка плагинов Android
Отладка приложений Android может быть выполнена с помощью Eclipse или Android Studio, хотя рекомендуется использовать Android Studio. Поскольку Cordova-Android в настоящее время используется как проект библиотеки, а плагины поддерживаются в виде исходного кода, можно отлаживать код Java внутри приложения Cordova так же, как и в обычном приложении Android.
Запуск других активностей
Необходимо учитывать некоторые особенности, если ваш плагин запускает активность, которая переводит активность Cordova в фоновый режим. ОС Android уничтожает активности в фоновом режиме, если на устройстве мало памяти. В этом случае экземпляр CordovaPlugin также будет уничтожен. Если ваш плагин ожидает результат от запущенной активности, новый экземпляр вашего плагина будет создан, когда активность Cordova будет возвращена в фоновый режим, и результат будет получен. Однако состояние плагина не будет автоматически сохранено или восстановлено, и CallbackContext для плагина будет потеряно. Существует два метода, которые ваш плагин CordovaPlugin может реализовать для обработки этой ситуации:
/**
* Called when the Activity is being destroyed (e.g. if a plugin calls out to an
* external Activity and the OS kills the CordovaActivity in the background).
* The plugin should save its state in this method only if it is awaiting the
* result of an external Activity and needs to preserve some information so as
* to handle that result; onRestoreStateForActivityResult() will only be called
* if the plugin is the recipient of an Activity result
*
* @return Bundle containing the state of the plugin or null if state does not
* need to be saved
*/
public Bundle onSaveInstanceState() {}
/**
* Called when a plugin is the recipient of an Activity result after the
* CordovaActivity has been destroyed. The Bundle will be the same as the one
* the plugin returned in onSaveInstanceState()
*
* @param state Bundle containing the state of the plugin
* @param callbackContext Replacement Context to return the plugin result to
*/
public void onRestoreStateForActivityResult(Bundle state, CallbackContext callbackContext) {}
Важно отметить, что вышеупомянутые методы следует использовать только в том случае, если ваш плагин запускает активность Activity для получения результата и должен восстанавливать только состояние, необходимое для обработки результата этой активности. Состояние плагина НЕ БУДЕТ восстановлено, за исключением случая, когда получен результат активности, запрошенный плагином с помощью метода CordovaInterface`s startActivityForResult(), а активность Cordova была уничтожена ОС во время работы в фоновом режиме.
В качестве части onRestoreStateForActivityResult(), вашему плагину будет передан заменяющий CallbackContext. Важно понимать, что этот CallbackContext НЕ ЯВЛЯЕТСЯ тем же, что был уничтожен вместе с активностью. Исходный коллбэк потерян и не будет вызван в приложении JavaScript. Вместо этого, этот заменяющий CallbackContext вернет результат в рамках события resume, которое срабатывает при возобновлении приложения. Данные события resume имеют следующую структуру:
{
action: "resume",
pendingResult: {
pluginServiceName: string,
pluginStatus: string,
result: any
}
}
-
pluginServiceNameбудет соответствовать элементу имя из вашего файла plugin.xml. -
pluginStatusбудет строкой, описывающей статус PluginResult, переданного CallbackContext. См. PluginResult.java для значений строк, соответствующих статусам плагина. -
resultбудет тем результатом, который плагин передает в CallbackContext (например, строка, число, объект JSON и т. д.).
Этот payload события resume будет передан любым коллбэкам, зарегистрированным приложением JavaScript для события resume. Это означает, что результат передается непосредственно в приложение Cordova; ваш плагин не сможет обработать результат с помощью JavaScript перед получением результата приложением. Следовательно, вы должны стремиться сделать возвращаемый результатом код максимально полным и не полагаться на JavaScript-коллбэки при запуске активностей.
Убедитесь, что вы указали, как приложение Cordova должно интерпретировать полученный результат в событии resume. Приложение Cordova должно самостоятельно поддерживать свое состояние и запоминать сделанные запросы и предоставленные аргументы, если это необходимо. Однако вы должны четко указать значение pluginStatus и какой тип данных возвращается в поле resume в рамках API вашего плагина.
Полная последовательность событий для запуска активности следующая
- Приложение Cordova вызывает ваш плагин
- Ваш плагин запускает активность для получения результата
- ОС Android уничтожает активность Cordova и экземпляр вашего плагина
onSaveInstanceState()вызывается
- Пользователь взаимодействует с вашей активностью, и активность завершается
- Активность Cordova воссоздается, и результат активности получен
onRestoreStateForActivityResult()вызывается
-
onActivityResult()вызывается, и ваш плагин передает результат в новый CallbackContext - Срабатывает событие
resume, которое получает приложение Cordova
Android предоставляет разработчикам настройку для отладки уничтожения активностей при малом объеме памяти. Включите настройку "Не сохранять активности" в меню "Параметры разработчика" на вашем устройстве или эмуляторе, чтобы смоделировать сценарии с малым объемом памяти. Если ваш плагин запускает внешние активности, вы всегда должны провести тестирование с этой настройкой, чтобы убедиться, что вы правильно обрабатываете сценарии с малым объемом памяти.
© 2012, 2013, 2015 The Apache Software Foundation
Licensed under the Apache License 2.0.
https://cordova.apache.org/docs/en/9.x/guide/platforms/android/plugin.html