Руководство по разработке плагинов для 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, плагин должен быть указан в файле приложения Cordova-Android res/xml/config.xml. Дополнительную информацию о том, как использовать файл 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.
Написание плагина Java для Android
Вызов 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 Dependency Management logic. Это позволяет использовать такие часто используемые библиотеки, как gson, android-support-v4 и google-play-services, несколькими плагинами без конфликтов.
Второй вариант — использовать тег <lib-file /> для указания расположения файла jar (подробнее см. в Спецификации плагина). Этот подход следует использовать только в том случае, если вы уверены, что ни один другой плагин не будет зависеть от библиотеки, на которую вы ссылаетесь (например, если библиотека специфична для вашего плагина). В противном случае вы рискуете вызвать ошибки сборки для пользователей вашего плагина, если другой плагин добавит ту же библиотеку. Стоит отметить, что разработчики приложений Cordova — это не обязательно разработчики нативных платформ, поэтому ошибки сборки нативной платформы могут быть особенно раздражающими.
Пример плагина Echo для Android
Чтобы соответствовать функции 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 объекта, указав первый параметр, переданный в метод. После передачи значения в частный метод echo, выполняется проверка параметров, чтобы убедиться, что это не null или пустая строка, в противном случае callbackContext.error() вызывает обратный вызов ошибки JavaScript. Если различные проверки пройдены, callbackContext.success() передаёт исходную строку message обратно в обратный вызов успеха JavaScript в качестве параметра.
Интеграция с Android
Android имеет систему Intent, которая позволяет процессам обмениваться данными. Плагины имеют доступ к объекту CordovaInterface, который может получить доступ к Android Activity, который выполняет приложение. Это контекст Context, необходимый для запуска нового Android Intent.
Плагины могут запускать 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/8.x/guide/platforms/android/plugin.html