Создание Native Extension для Android на Java и ActionScript 3

2
Наверняка, многие, кто разрабатывает мобильные игры на AIR используют расширения называемые Native Extension. Но не все знают, как эти расширения делаются. Об этом я расскажу в данном уроке. Материал очень подробный, но простым я его назвать не могу. Установку и настройку FlashDevelop + Flex SDK + AIR SDK я опущу, в отличии от установки и настройки Eclipse + Android SDK.

Что такое Native Extension и зачем он нужен?


Adobe AIR с версии 3.0 поддерживает специальные расширения(собственно Native Extensions). Они позволяют AS3 взаимодействовать с собственным кодом платформы, под которую написано AIR приложение. Нативный язык позволяет полноценно работать с api операционной системы, а AS3 может взаимодействовать с нативными кодом. То есть можно работать с любыми функциями API платформы через AS3. На данный момент поддерживаются Windows, OSX, iOS и Android. Мы будем писать небольшое расширение на Java+AS3 под платформу Android. Делается это следующим образом: пишется AS3 код, пишется java код, запаковывается в библиотеки и специальным образом компилируется через adt. Звучит просто, но на деле всё немного сложней. Потребуется довольно большое количество инструментов и различных настроек.

Что именно будем делать?


Небольшое расширение под кодовым названием «Notification Extension». Оно будет иметь две функции: показ диалога с кнопкой «OK» и показ тоста. Toast — небольшое всплывающее сообщение в Android. То и другое входит в Android API.
Выглядят они следующим образом:


Какие инструменты понадобятся?


— JDK + JRE 6 update 38. Соглашаемся с лицензией, качаем версию Windows x86.
— Ecliple 4.2. Подойдёт версия Classic или Eclipse IDE for Java Developers. Я использую второе.
— Android SDK
— FlashDevelop 4.2.1 для работы с as3.
— Flash Professional CS6. Нужен для компиляции swc. Ибо способа компиляции swc с поддержкой AIR классов во FlashDevelop я не нашёл. В конце статьи есть ссылка на другой способ сборки.
— Flex SDK 4.6
— AIR SDK 3.5
— AIR Runtime for Android
— 7-zip для распаковки swc.

Версии указаны актуальные на момент написания урока.

Установка и настройка


Устанавливаем JDK + JRE. Тут всё стандартно и просто.
Устанавливаем Android SDK. В пуске появится папка Android SDK Tools. Запускаем SDK Manager из неё. Ждём, пока обновится информация (прогрессбар внизу).
Выделяем то же самое, что показано на скриншоте и нажимаем кнопку Install Packages, соглашаемся со всеми лицензиями, ждём, пока скачаются все компоненты.



Далее распаковываем и запускаем Eclipse. В него нужно установить специальный плагин для работы с Android.
В главном меню программы нажимаем Help/Install New Software. В строку Work with вставляем ссылку:

https://dl-ssl.google.com/android/eclipse/

Жмём кнопку Add. Появлятся Developer Tool и NDK Plugin. Выделяем Developer Tool, жмём Next, затем Finish. Ждём, пока плагин скачается и установится. Если возникает ошибка, поменяйте в ссылке https на http.



После установки плагина Eclipse нужно перезапустить.

Устанавливаем FlashDevelop и FlashProfessional(на него лучше накатить Update 2).
Качаем Flex SDK и AIR SDK. Распаковываем Flex SDK и в его папку распаковываем AIR SDK с заменой файлов. Подключаем Flex SDK к FlashDevelop.
Устанавливаем 7-zip.
Устанавливаем AIR Runtime на Android устройство.

Осталось только прописать переменные среды. Открываем компьютер. Правый клик по пустому месту, в меню выбираем Properties, жмём Advanced System Settings, нажимаем кнопку Enviroment Variables. Находим переменную Path и открываем её. Нужно в конце добавить пути к java и к adt.
Пути выглядят примерно так:
c:\Program Files (x86)\Java\jre6\bin\
c:\Program Files (x86)\FlexSDK\4.6.0\bin\

Подкорректируйте пути, чтобы они соответствовали действительности и добавьте в конце значения Path следующее:

;c:\Program Files (x86)\Java\jre6\bin\;c:\Program Files (x86)\FlexSDK\4.6.0\bin\



С установкой и настройкой всё. Подготавливаем проекты.

Создайте где-нибудь папку с названием NotificationExtension. В ней создайте ещё четыре папки:
EclipseProject
FlashDevelopProject
FlashProfessionalProject
ane
Это не обязательно. Просто для порядка.

Создаём Eclipse проект и пишем java код


Открываем Eclipse. File/New/Other/Android Application Project. На первом экране выставляем настройки:



Жмём Next, снимаем галочки с Create Activity, Create custom launcher icon и Create Project in Workspace. В Location указываем путь к нашей папке NotificationExtension/EclipseProject. Жмём Finish.



Проект создан. К нему нужно подключить специальную библиотеку от Adobe. Правый клик по проекту, properties/Java Build Path. Нажимаем Add External Jars, идём в папку ...FlexSDK/4.6./lib/android, выбираем там FlashRuntimeExtensions.jar и нажимаем OK.
Получается примерно такой проект:



Создаём первый класс(правой кнопкой по srcNew/Class). Он является точкой входа приложение и имплементирует com.adobe.fre.FREExtension из библиотеки, которую мы подключали к проекту.

package com.illuzor.notificationextension;

import com.adobe.fre.FREContext;
import com.adobe.fre.FREExtension;

public class NotificationExtension implements FREExtension {

        @Override
        public FREContext createContext(String arg0) {
                return new NotificationExtensionContext();
        }

        @Override
        public void dispose() {
                // TODO Auto-generated method stub
        }

        @Override
        public void initialize() {
                // TODO Auto-generated method stub
        }

}

Метод createContext возвращает экземпляр класса NotificationExtensionContext, который мы создадим сейчас(наводим курсор на NotificationExtensionContext и жмём Create Class):

package com.illuzor.notificationextension;

import java.util.HashMap;
import java.util.Map;

import com.adobe.fre.FREContext;
import com.adobe.fre.FREFunction;

public class NotificationExtensionContext extends FREContext {

        @Override
        public void dispose() {
                // TODO Auto-generated method stub
        }

        @Override
        public Map<String, FREFunction> getFunctions() {
                Map<String, FREFunction> map = new HashMap<String, FREFunction>();
                map.put("showToast", new ShowToastFuntion());
                map.put("showAlert", new ShowAlertFuntion());
                return map;
        }

}

Тут уже интересней. Класс расширяет FREContext из той же самой библиотеки. В методе getFunctions создаётся список соответствия функций и их строковых значений. Каждая функция представляет собой класс, имплементирующий FREFunction. Из AS3 функция вызывается по строковому значению, подобно ExternalInterface.call().

Создаём классы функций(возможно, немного странно звучит, но так оно и есть):

package com.illuzor.notificationextension;

import android.content.Context;
import android.util.Log;
import android.widget.Toast;

import com.adobe.fre.FREContext;
import com.adobe.fre.FREFunction;
import com.adobe.fre.FREObject;

public class ShowToastFuntion implements FREFunction {

        @Override
        public FREObject call(FREContext context, FREObject[] args) {
                try {
                        String message = args[0].getAsString();
                        Context toastContext = context.getActivity();
                        Toast toast = Toast.makeText(toastContext, message, Toast.LENGTH_SHORT);
                        toast.show();
                        
                        Log.i("Notification Extension", "Toast OK");
                        
                } catch (Exception e) {
                        e.printStackTrace();
                        Log.i("Notification Extension", "Toast Error");
                } 
                
                return null;
        }

}

Это класс для показа тоста. Текстовая переменная message берёт значение из параметра args, который представляет собой массив данных, переданных из AS3. Дальше идёт код отображения тоста. Через try/catch отлавливаются возможные ошибки этапа выполнения.

package com.illuzor.notificationextension;

import android.app.AlertDialog;
import android.content.DialogInterface;
import android.util.Log;

import com.adobe.fre.FREContext;
import com.adobe.fre.FREFunction;
import com.adobe.fre.FREObject;

public class ShowAlertFuntion implements FREFunction {

        @Override
        public FREObject call(FREContext context, FREObject[] args) {
                try {
                        String alertTitle = args[0].getAsString();
                        String alertText = args[1].getAsString();
                        
                        AlertDialog.Builder alertBuilder = new AlertDialog.Builder(context.getActivity());

                        alertBuilder.setTitle(alertTitle);
                        alertBuilder.setMessage(alertText);
                        
                        alertBuilder.setNeutralButton("OK", new DialogInterface.OnClickListener() {
                                public void onClick(DialogInterface dialog, int id) {
                                        // close dialog
                                }
                        });
                        
                        AlertDialog alertDialog = alertBuilder.create();
                        alertDialog.show();
                        
                        Log.i("Notification Extension", "Alert OK");
                        
                } catch (Exception e) {
                        e.printStackTrace();
                        Log.i("Notification Extension", "Alert Error");
                } 

                return null;
        }

}

Класс функции для отображения диалога. Почти аналогично классу тоста, только параметров два — заголовок диалога и его основной текст. Функция отображает диалог с заданным заголовком, текстом сообщения и кнопкой ОК, которая закрывает диалог.

В обоих классах есть вывод данных в лог через Log.i(). К этому вернёмся позже.

Подводя итог. Общие правила для написания расширений следующие:
— Точкой входа должен быть класс, имплементирующий FREExtension
— Второй обязательный класс должен наследоваться от FREContext и содержать список соответствий классов функций и их строковых значений.
— Каждая функция представляет собой класс, имплементирующий FREFunction. Параметры функции берутся из массива FREObject[] args.

Идём в нашу папку NotificationExtension/ane, создаём в ней ещё одну папку под названием android.

Возвращаемся в Eclipse, правой кнопкой по пакету, Export:



Выбираем Java/Jar file, жмём Next. Убеждаемся, что настройки соответствуют скриншотам:



Особое внимание уделите пути. Сохранять jar нужно в папку NotificationExtension\ane\android. Имя файла — notifLib.jar.
Нажимаем Finish.
Если при экспорте возникла ошибка, закрываем окно экспорта, в главном меню выбираем Project/Clean/Ok. После этого экспортируем заново.
Сам по себе Eclipse довольно глючный, так что не удивляйтесь всяким непонятным багам.

На этом можно закрыть Ecliplse. Больше они нам не нужен.

Пишем AS3 код.


Открываем FlashDevelop и создаём новый проект AIR Mobile as3 app в папке NotificationExtension\FlashDevelopProject



Класс Main перемещаем в корень папки src, чтоб не мешался. В пакете com.illuzor.notificationextension создаём класс NotificationExtension.
Этот класс — AS3 часть расширения, который нужен для взаимодействия AS3 и java. Он довольно простой:

package com.illuzor.notificationextension {
        
        import flash.external.ExtensionContext;

        public class NotificationExtension {
                
                private static var context:ExtensionContext;
                
                public static function init():void {
                        if(!context) context = ExtensionContext.createExtensionContext("com.illuzor.Notification", null);
                }
                
                public static function showToast(toastText:String):void {
                        if (context) context.call("showToast", toastText);
                }
                
                public static function showAlert(title:String, text:String):void {
                        if (context) context.call("showAlert", title, text);
                }
                
        }
}

При инициализации создаётся экземпляр класса ExtensionContext, который позволяет вызывать нативные функции (java в нашем случае).
В функции showToast происходит вызов java функции "showToast" по строковому значению и передача текста для отображения в виде параметра.
В showalert аналогично, только параметров два. Очень важный момент — строковые значения в java и в as3 обязательно должны совпадать:



Заходим в папку src, копируем папку com. Идём в папку NotificationExtension\FlashProfessionalProject и вставляем туда com.

Открываем Flash Professional, создаём новый проект AIR for Android. File/Save As — сохраняем в NotificationExtension\FlashProfessionalProject под именем lib.fla. Заходим в File/Publish Settings, выставляем настройки:



В первом кадре пишем следующий код:

import com.illuzor.notificationextension.NotificationExtension;
NotificationExtension;


Жмём File/Publish. В папке с проектом появится некоторые количество файлов. Нам нужен только lib.swc. Можно удалить всё, кроме swc, fla и папки com. Swc представляет собой обычный zip, распаковываем его через 7-zip. Внутри два файла, нам нужен только library.swf. После всех манипуляций в папке FlashProfessionalProject дожны остаться следующие файлы:



С файлами library.swf и lib.swc нужно проделать ещё некоторые манипуляции. Идём в папку NotificationExtension\ane, создаём там папку default. Копируем файл lib.swc в NotificationExtension\ane. Копируем library.swf в папки NotificationExtension\ane\android и в NotificationExtension\ane\default.
Должна получиться вот такая структура:



XML описание расширения.


В папке NotificationExtension\ane создаём файл extension.xml с таким содержанием:

<extension xmlns="http://ns.adobe.com/air/extension/3.5">
        <id>com.illuzor.Notification</id>
        <versionNumber>1</versionNumber>
        <platforms>
                <platform name="Android-ARM">
                        <applicationDeployment>
                                <nativeLibrary>notifLib.jar</nativeLibrary>
                                <initializer>com.illuzor.notificationextension.NotificationExtension</initializer>
                                <finalizer>com.illuzor.notificationextension.NotificationExtension</finalizer>
                        </applicationDeployment>
                </platform>
        </platforms>
</extension>


id — это идентификатор. У каждого расширения он уникальный. Именно по этому идентификатору создаётся контекст в AS3 коде расширения.
versionNumber — версия расширения. Нам это не очень важно.
nativeLibrary — имя jar библиотеки.
initializer и finalizer — полное имя класса-точки входа.

Компиляция расширения.


Теперь с помощью командной строки нужно скомпилировать расширение в ane файл. Я пользуюсь следующим способом — захожу в папку NotificationExtension\ane через Total Commander и снизу в командную строку ввожу команду. Можно сделать по-другому, жмём win+r, вводим cmd, жмём enter. Вручную вводим путь к NotificationExtension\ane и вводим команду компиляции.
Вот сама команда:

adt -package -target ane notifExtension.ane extension.xml -swc lib.swc -platform Android-ARM -C android . -platform default -C default

После её ввода в папке NotificationExtension\ane должен появиться файл notifExtension.ane. Это и есть скомпилированное расширение (Native Extension). Если файл не появился или выдаётся ошибка, значит не прописаны переменные среды.

Наше расширение готово. Можно начинать радоваться. Осталось только написать небольшое демонстрационное приложение.
Переименовываем расширение в com.illuzor.Notification.ane (это идентификатор) и кладём его в папку lib нашего FlashDevelop проекта.

Создание демонстрационного приложения.


Приложение уже создано (вы ведь не забыли об этом? FlashDevelop проект). Нужно его немного настроить. Project/Properties — выставляем Platform значение AIR Mobile 3.5. Открываем application.xml, там в первой строке тоже выставляем версию 3.5. Project/AIR App properties — Выставляем следующие настройки:



bat/CreateCertificate.bat — правой кнопкой и Execute, создастся сертификат для приложения.
Открываем bat/Packager.bat, ищем строку, которая начинается на call adt и добавляем в конец "-extdir lib/"

Было:
call adt -package -target %TYPE%%TARGET% %OPTIONS% %SIGNING_OPTIONS% "%OUTPUT%" "%APP_XML%" %FILE_OR_DIR%
Стало:
call adt -package -target %TYPE%%TARGET% %OPTIONS% %SIGNING_OPTIONS% "%OUTPUT%" "%APP_XML%" %FILE_OR_DIR% -extdir lib/

В папке lib лежит файл com.illuzor.Notification.ane. Правой кнопкой по нему — Add to library. Ещё раз правой кнопкой — Options — выставляем там External Library(not included).

Теперь можно писать код. Приложение состоит из двух классов:

package {
        
        import com.illuzor.notificationextension.NotificationExtension;
        import flash.desktop.NativeApplication;
        import flash.display.Shape;
        import flash.display.Sprite;
        import flash.display.StageAlign;
        import flash.display.StageScaleMode;
        import flash.events.Event;
        import flash.events.MouseEvent;
        import flash.geom.Rectangle;
        import flash.text.StageText;
        import flash.ui.Multitouch;
        import flash.ui.MultitouchInputMode;
        
        public class Main extends Sprite {
                
                private var alertButton:AppButton;
                private var toastButton:AppButton;
                private var titleTextField:StageText;
                private var textTextField:StageText;
                
                public function Main():void {
                        stage.scaleMode = StageScaleMode.NO_SCALE;
                        stage.align = StageAlign.TOP_LEFT;
                        stage.addEventListener(Event.DEACTIVATE, deactivate);

                        Multitouch.inputMode = MultitouchInputMode.TOUCH_POINT;
                        
                        NotificationExtension.init();
                        
                        titleTextField = generateTextField(new Rectangle(15, 15, stage.stageWidth - 30, 80), "Enter title");
                        textTextField = generateTextField(new Rectangle(15, 120, stage.stageWidth - 30, 80), "Enter text");
                        
                        toastButton = new AppButton("Show Toast");
                        addChild(toastButton);
                        toastButton.x = (stage.stageWidth - toastButton.width) / 2;
                        toastButton.y = 250;
                        
                        alertButton = new AppButton("Show Alert");
                        addChild(alertButton);
                        alertButton.x = (stage.stageWidth - alertButton.width) / 2;
                        alertButton.y = 330;
                        
                        toastButton.addEventListener(MouseEvent.CLICK, onButtonClick);
                        alertButton.addEventListener(MouseEvent.CLICK, onButtonClick);
                }
                
                private function onButtonClick(e:MouseEvent):void {
                        if (e.currentTarget == alertButton) NotificationExtension.showAlert(titleTextField.text, textTextField.text);
                        if (e.currentTarget == toastButton) NotificationExtension.showToast(textTextField.text);
                }

                private function generateTextField(rect:Rectangle, initialText:String):StageText {
                        
                        var shape:Shape = new Shape();
                        shape.graphics.beginFill(0xD2D2D2);
                        shape.graphics.drawRect(rect.x - 5, rect.y - 5, rect.width + 10, rect.height +10);
                        shape.graphics.endFill();
                        addChild(shape);
                        
                        var stageText:StageText = new StageText();
                        stageText.fontSize = 25;
                        stageText.stage = stage;
                        stageText.editable = true;
                        stageText.viewPort = rect;
                        stageText.text = initialText;
                        return stageText;
                }
                
                private function deactivate(e:Event):void {
                        NativeApplication.nativeApplication.exit();
                }
                
        }
}

Основной класс. Создаются два текстовых поля для ввода текста и две кнопки — одна для отображения тоста, другая для отображения диалога. Текст берётся из текстовых полей.

package  {
        
        import flash.display.Shape;
        import flash.display.Sprite;
        import flash.events.Event;
        import flash.events.MouseEvent;
        import flash.text.TextField;
        import flash.text.TextFieldAutoSize;
        import flash.text.TextFormat;
        
        public class AppButton extends Sprite {
                
                private var shape:Shape;
                private var text:String;
                
                public function AppButton(text:String) {
                        this.text = text;
                        addEventListener(Event.ADDED_TO_STAGE, onAdded);
                }
                
                private function onAdded(e:Event):void {
                        removeEventListener(Event.ADDED_TO_STAGE, onAdded);
                        shape = new Shape();
                        addChild(shape);
                        
                        var tf:TextFormat = new TextFormat();
                        tf.size = 25;
                        
                        var textField:TextField = new TextField();
                        textField.mouseEnabled = false;
                        textField.autoSize = TextFieldAutoSize.LEFT;
                        textField.defaultTextFormat = tf;
                        textField.text = text;
                        textField.x = (280 - textField.width) / 2;
                        textField.y = 10;
                        addChild(textField);
                        drawShape(0x008080);
                        
                        addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
                        stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);
                }
                
                private function onMouseDown(e:MouseEvent):void {
                        drawShape(0x00E6E6);
                }
                
                private function onMouseUp(e:MouseEvent):void {
                        drawShape(0x008080);
                }
                
                private function drawShape(color:uint):void {
                        shape.graphics.clear();
                        shape.graphics.beginFill(color);
                        shape.graphics.drawRect(0, 0, 280, 50);
                        shape.graphics.endFill();
                }
                
        }
}

Класс кнопки, тут вообще всё элементарно.

Можно компилировать и заливать на девайс.
Таким вот образом из AIR приложения можно использовать практически всё, что можно использовать в Java. Например, можно встроить внутреигровые покупки в AIR игру. Расширения сразу для нескольких платформ можно скомпилировать в один ane файл.

Логи Java кода.


В Android SDK есть специальный инструмент для отладки приложений под названием Dalvik Debug Monitor. Dalvik — это виртуальная машина андроида, в которой выполняются все java приложения. Чтобы его запустить, идём в папку C:\Program Files\Android SDK\tools и запускаем файл ddms.bat. Если девайс подключен к компьютеру кабелем, он отобразится в списке, если не подключен — подключите.



Чтобы увидеть лог в нашем случае нужно нажать одну из кнопок в приложении:



Также монитор выводит все логи всех остальных приложений.

UPD. Сборка ane с помощью ant.

На этом урок окончен, спасибо за внимание.

Ссылка на исходники.
Ссылка на приложение.
Ссылка на github


(Прямая ссылка на приложение)

Если у вас есть вопросы, мои контактные данные в профиле, пишите, не стесняйтесь.
  • +24

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

0
Скажи, с тех пор ничего такого не изменилось, чтобы статья стала неактуальной?
Я просто сейчас по ней как по учебнику буду шпилить )
0
Всё актуально. Разве что софт обновился и интерфейс местами поменялся.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.