*** Adobe совершенно не поддерживает следующее *** Adobe AIR предоставляет согласованную платформу для настольных и мобильных приложений. Хотя согласованность очень важна, бывают случаи, когда разработчикам необходимо выходить за рамки общих API. В этой статье вы узнаете, как интегрировать приложения AIR for Android с другими встроенными API и функциями в Android SDK. Он охватывает три распространенных варианта использования для собственной расширяемости: системные уведомления, виджеты и лицензирование приложений.
Если вы хотите следовать, вам понадобятся следующие предварительные условия:
- Adobe Flash Builder 4.5 (который включает в себя Flex 4.5 SDK и AIR 2.6 SDK)
- Android SDK
- Плагин Android Eclipse
Прежде чем начать, немного фона поможет. Приложения Android распространяются в виде файлов APK. Файл APK содержит исполняемый файл Dalvik (dex), который будет работать на устройстве Android внутри виртуальной машины Dalvik. Android SDK компилирует Java-подобный язык для dex.
Приложения AIR for Android также распространяются в виде файлов APK. Внутри этих APK-файлов есть небольшая часть кода, которая загружает среду выполнения AIR for Android, которая затем загружает и запускает SWF-файл, который также находится внутри APK. Фактический класс dex, который загружает приложение AIR, динамически генерируется инструментом adt в AIR SDK. Класс называется AppEntry, и его имя пакета зависит от идентификатора приложения AIR, но всегда начинается с «air». Класс AppEntry проверяет существование среды выполнения AIR, а затем запускает приложение AIR. Файл дескриптора Android в AIR APK указывает, что основным классом приложения является класс AppEntry.
Чтобы расширить приложения AIR для Android для включения собственных API-интерфейсов и функциональности Android SDK, начните с создания SWF-файла с помощью Flex, а затем скопируйте этот SWF-файл, классы dex для AIR для Android и необходимые ресурсы в стандартный проект Android. Используя оригинальный класс AppEntry, вы все равно можете загрузить приложение AIR в проекте Android, но вы можете расширить этот класс, чтобы получить хук запуска.
- Для начала загрузите пакет с необходимыми зависимостями для расширения AIR для Android:
http://www.jamesward.com/downloads/extending_air_for_android-flex_4_5-air_2_6-v_1.zip - Затем создайте обычный проект Android в Eclipse (пока не создавайте Activity):
- Скопируйте все файлы из zip-файла, который вы загрузили, в корневой каталог недавно созданного проекта Android. Вам нужно будет перезаписать существующие файлы и обновить конфигурацию запуска (если вас об этом попросит Eclipse).
- Удалите каталог «res / layout».
- Добавьте файл airbootstrap.jar в путь сборки проекта. Вы можете сделать это, щелкнув правой кнопкой мыши по файлу, затем выбрать Build Path и затем Add to Build Path.
- Убедитесь, что проект запущен. Вы должны увидеть «привет, мир» на вашем Android-устройстве. Если это так, то приложение AIR правильно загружается, а приложение Flex в assets / app.swf выполняется правильно.
На этом этапе, если вам не нужны какие-либо пользовательские перехватчики запуска, вы можете просто заменить файл assets / app.swf своим собственным SWF-файлом (но он должен называться app.swf). Если вам нужен пользовательский хук запуска, просто создайте новый класс Java с именем «MainApp», который расширяет класс air.app.AppEntry.
- Переопределите метод onCreate () и добавьте собственную логику запуска перед вызовом super.onCreate () (который загружает приложение AIR). Вот пример:
package com.jamesward; import air.app.AppEntry; import android.os.Bundle; public class MainApp extends AppEntry { @Override public void onCreate(Bundle arg0) { System.out.println("test test"); super.onCreate(arg0); } }
Откройте файл дескриптора AndroidManifest.xml и попросите его использовать новый класс MainApp вместо исходного класса AppEntry. Сначала измените пакет так, чтобы он совпадал с пакетом вашего MainApp:
<manifest package="com.jamesward" android:versionCode="1000000" android:versionName="1.0.0" xmlns:android="http://schemas.android.com/apk/res/android">
Также обновите действие, чтобы использовать класс MainApp (убедитесь, что у вас есть точка перед именем класса):
-
<activity android:name=".MainApp"
Вы также можете добавить любые другие разрешения или настройки, которые могут вам понадобиться, в файле AndroidManifest.xml.
- Сохраните изменения и, когда Eclipse предложит вам, обновите конфигурацию запуска.
- Запустите приложение, и вы снова увидите «Привет, мир». На этот раз, однако, в LogCat (инструмент командной строки или представление Eclipse) вы должны увидеть результат «test test». Теперь, когда у вас есть хук для стартапа, вы можете делать забавные вещи!
Системные уведомления и сервисы
Приложения AIR for Android еще не имеют API для уведомлений системы Android. Но вы можете добавить системные уведомления в приложение AIR for Android с помощью кнопки запуска. Чтобы приложение AIR могло взаимодействовать с собственными API-интерфейсами Android, необходимо указать мост для связи. Самый простой способ создать этот мост — использовать сетевой сокет. Приложение Android может прослушивать данные в сокете, а затем читать эти данные и определять, нужно ли отображать системное уведомление. Затем приложение AIR может подключиться к сокету и отправить необходимые данные. Это довольно простой пример, но должна быть реализована некоторая защита (например, обмен ключами), чтобы гарантировать, что вредоносные приложения не обнаружат сокет и не злоупотребят им. Также, вероятно, потребуется некоторая логика для определения того, какой сокет следует использовать.
- В разделе приложения добавьте новый Android-сервис:
- Поскольку в этом примере используется Socket, вам также необходимо добавить разрешение INTERNET:
- Вы также можете включить вибрацию телефона при появлении нового уведомления. Если это так, добавьте это разрешение:
<uses-permission android:name="android.permission.VIBRATE"/>
- Сохраните ваши изменения в AndroidManifest.xml.
- Затем создайте фоновый класс Java Service, который называется TestService. Этот сервис будет прослушивать сокет и при необходимости отображать уведомление Android:
package com.jamesward; import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.IBinder; import android.os.Looper; import android.util.Log; public class TestService extends Service { private boolean stopped=false; private Thread serverThread; private ServerSocket ss; @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); Log.d(getClass().getSimpleName(), "onCreate"); serverThread = new Thread(new Runnable() { public void run() { try { Looper.prepare(); ss = new ServerSocket(12345); ss.setReuseAddress(true); ss.setPerformancePreferences(100, 100, 1); while (!stopped) { Socket accept = ss.accept(); accept.setPerformancePreferences(10, 100, 1); accept.setKeepAlive(true); DataInputStream _in = null; try { _in = new DataInputStream(new BufferedInputStream(accept.getInputStream(),1024)); } catch (IOException e2) { e2.printStackTrace(); } int method =_in.readInt(); switch (method) { // notification case 1: doNotification(_in); break; } } } catch (Throwable e) { e.printStackTrace(); Log.e(getClass().getSimpleName(), "Error in Listener",e); } try { ss.close(); } catch (IOException e) { Log.e(getClass().getSimpleName(), "keep it simple"); } } },"Server thread"); serverThread.start(); } private void doNotification(DataInputStream in) throws IOException { String id = in.readUTF(); displayNotification(id); } @Override public void onDestroy() { stopped=true; try { ss.close(); } catch (IOException e) {} serverThread.interrupt(); try { serverThread.join(); } catch (InterruptedException e) {} } public void displayNotification(String notificationString) { int icon = R.drawable.mp_warning_32x32_n; CharSequence tickerText = notificationString; long when = System.currentTimeMillis(); Context context = getApplicationContext(); CharSequence contentTitle = notificationString; CharSequence contentText = "Hello World!"; Intent notificationIntent = new Intent(this, MainApp.class); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); Notification notification = new Notification(icon, tickerText, when); notification.vibrate = new long[] {0,100,200,300}; notification.setLatestEventInfo(context, contentTitle, contentText, contentIntent); String ns = Context.NOTIFICATION_SERVICE; NotificationManager mNotificationManager = (NotificationManager) getSystemService(ns); mNotificationManager.notify(1, notification); } }
Эта служба прослушивает порт 12345. Когда она получает некоторые данные, она проверяет, является ли первый отправленный «int» «1». Если это так, то он создает новое уведомление, используя следующий фрагмент данных (строку), полученный через сокет.
- Измените Java-класс MainApp, чтобы запустить службу при вызове метода onCreate ():
@Override public void onCreate(Bundle savedInstanceState) { try { Intent srv = new Intent(this, TestService.class); startService(srv); } catch (Exception e) { // service could not be started } super.onCreate(savedInstanceState); }
Это все, что вам нужно сделать в приложении для Android.
- Затем создайте приложение Flex, которое подключится к сокету и отправит нужные данные. Вот пример кода для моего класса Notifier.mxml, который я использовал для тестирования службы Android:
<?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark"> <fx:Style> @namespace s "library://ns.adobe.com/flex/spark"; global { fontSize: 32; } </fx:Style> <s:layout> <s:VerticalLayout horizontalAlign="center" paddingTop="20"/> </s:layout> <s:TextInput id="t" text="test test"/> <s:Button label="create notification"> <s:click> <![CDATA[ var s:Socket = new Socket(); s.connect("localhost", 12345); s.addEventListener(Event.CONNECT, function(event:Event):void { trace('connected!'); (event.currentTarget as Socket).writeInt(1); (event.currentTarget as Socket).writeUTF(t.text); (event.currentTarget as Socket).flush(); (event.currentTarget as Socket).close(); }); s.addEventListener(IOErrorEvent.IO_ERROR, function(event:IOErrorEvent):void { trace('error! ' + event.errorID); }); s.addEventListener(ProgressEvent.SOCKET_DATA, function(event:ProgressEvent):void { trace('progress '); }); ]]> </s:click> </s:Button> </s:Application>
Как вы можете видеть, есть только элемент управления TextInput, который позволяет пользователю вводить некоторый текст. Затем, когда пользователь нажимает кнопку, приложение AIR for Android подключается к локальному сокету через порт 12345, записывает int со значением 1, записывает строку, введенную пользователем в элемент управления TextInput, и, наконец, сбрасывает и закрывает соединение. Это приводит к отображению уведомления.
- Теперь просто скомпилируйте приложение Flex и перезапишите файл assets / app.swf новым приложением Flex. Посмотрите видео демонстрацию этого кода.
<service android:enabled="true" android:name="TestService" />
<uses-permission android:name="android.permission.INTERNET"/>
Виджеты
Виджеты в Android — это мини-приложения, которые могут отображаться на главном экране устройства. Существует довольно ограниченное количество вещей, которые могут отображаться в виджетах. Так что, к сожалению, виджеты не могут быть построены с AIR для Android. Однако пользовательский виджет приложения может быть упакован с приложением AIR for Android. Чтобы добавить виджет в приложение AIR for Android, можно использовать класс AppEntry по умолчанию, а не оборачивать его другим классом (в моем примере MainApp). (Однако не мешает держать там класс MainApp.) Чтобы добавить виджет, просто добавьте его определение в файл AndroidManifest.xml, создайте виджет с Java и создайте соответствующий ресурс макета.
- Сначала определите виджет в разделе приложения файла AndroidManifest.xml:
<receiver android:name=".AndroidWidget" android:label="app"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/airandroidwidget" /> </receiver>
- Вам нужен ресурс XML, который предоставляет метаданные о виджете. Просто создайте новый файл с именем airandroidwidget.xml в новом каталоге res / xml со следующим содержимым:
<?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="294dp" android:minHeight="72dp" android:updatePeriodMillis="86400000" android:initialLayout="@layout/main"> </appwidget-provider>
Это говорит виджету использовать основной ресурс макета в качестве исходного макета для виджета.
- Создайте файл res / layout / main.xml, который содержит простой текстовый дисплей:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/widget" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#ffffffff" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="hello" /> </LinearLayout>
Затем вам нужно создать класс AppWidgetProvider, указанный в файле AndroidManifest.xml.
- Создайте новый класс Java с именем AndroidWidget со следующим содержимым:
package com.jamesward; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; import android.widget.RemoteViews; import com.jamesward.MainApp; public class AndroidWidget extends AppWidgetProvider { public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { final int N = appWidgetIds.length; // Perform this loop procedure for each App Widget that belongs to this provider for (int i=0; i<N; i++) { int appWidgetId = appWidgetIds[i]; Intent intent = new Intent(context, MainApp.class); intent.setAction(Intent.ACTION_MAIN); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.main); views.setOnClickPendingIntent(R.id.widget, pendingIntent); appWidgetManager.updateAppWidget(appWidgetId, views); } } }
Этот класс будет отображать виджет при необходимости и регистрировать обработчик кликов, который будет открывать приложение MainApp, когда пользователь нажимает на виджет.
- Запустите приложение, чтобы убедиться, что оно работает.
- Теперь вы можете добавить виджет на домашний экран, удерживая его на главном экране и следуя указаниям мастера виджетов.
- Убедитесь, что при нажатии на виджет запускается приложение AIR.
Лицензирование приложений
Android предоставляет API-интерфейсы, которые помогут вам обеспечить соблюдение политик лицензирования несвободных приложений в Android Market. Возможно, вы захотите прочесть о лицензировании Android, прежде чем попробовать.
Чтобы добавить лицензирование приложения в приложение AIR for Android, сначала необходимо выполнить действия, описанные в документации Android. Основные шаги заключаются в следующем:
- Настройка учетной записи издателя Android Market
- Установите пакет лицензирования Market в Android SDK
- Создайте новый проект библиотеки Android LVL в Eclipse
- Добавить ссылку на библиотеку в проекте Android в библиотеку LVL Android
- Добавьте разрешение CHECK_LICENSE в файл манифеста вашего проекта Android:
<uses-permission android:name="com.android.vending.CHECK_LICENSE" />
После выполнения этих шагов настройки вы готовы обновить Java-класс MainApp для обработки проверки лицензии:
package com.jamesward; import com.android.vending.licensing.AESObfuscator; import com.android.vending.licensing.LicenseChecker; import com.android.vending.licensing.LicenseCheckerCallback; import com.android.vending.licensing.ServerManagedPolicy; import air.Foo.AppEntry; import android.os.Bundle; import android.os.Handler; import android.provider.Settings.Secure; public class MainApp extends AppEntry { private static final String BASE64_PUBLIC_KEY = "REPLACE WITH KEY FROM ANDROID MARKET PROFILE"; // Generate your own 20 random bytes, and put them here. private static final byte[] SALT = new byte[] { -45, 12, 72, -31, -8, -122, 98, -24, 86, 47, -65, -47, 33, -99, -55, -64, -114, 39, -71, 47 }; private LicenseCheckerCallback mLicenseCheckerCallback; private LicenseChecker mChecker; private Handler mHandler; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mHandler = new Handler(); String deviceId = Secure.getString(getContentResolver(), Secure.ANDROID_ID); mLicenseCheckerCallback = new MyLicenseCheckerCallback(); mChecker = new LicenseChecker( this, new ServerManagedPolicy(this, new AESObfuscator(SALT, getPackageName(), deviceId)), BASE64_PUBLIC_KEY); mChecker.checkAccess(mLicenseCheckerCallback); } private void displayFault() { mHandler.post(new Runnable() { public void run() { // Cover the screen with a messaging indicating there was a licensing problem setContentView(R.layout.main); } }); } private class MyLicenseCheckerCallback implements LicenseCheckerCallback { public void allow() { if (isFinishing()) { // Don't update UI if Activity is finishing. return; } // Should allow user access. } public void dontAllow() { if (isFinishing()) { // Don't update UI if Activity is finishing. return; } displayFault(); } public void applicationError(ApplicationErrorCode errorCode) { if (isFinishing()) { // Don't update UI if Activity is finishing. return; } } } @Override protected void onDestroy() { super.onDestroy(); mChecker.onDestroy(); } }
Также добавьте следующее в новый файл res / layout / main.xml, чтобы отобразить ошибку при отказе в лицензии:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/license_problem" /> </LinearLayout>
Текст для отображения использует строковый ресурс с именем «license_problem», который необходимо добавить в файл res / values / strings.xml:
<string name="license_problem">THERE WAS A PROBLEM LICENSING YOUR APPLICATION!</string>
Когда приложение запустится, оно проверит действующую лицензию. Если лицензия вернется в действие, приложение AIR запустится и будет работать как обычно. Однако при наличии недопустимой лицензии приложение установит для ContentView ресурс R.layout.main, который отображает сообщение об ошибке, определенное в ресурсе license_problem. Чтобы смоделировать различные ответы, вы можете изменить «Тестовый ответ» в своем профиле Android Market.
Детали Gory
Я обернул сгенерированный класс AppEntry и его ресурсы, чтобы сделать процесс расширения AIR for Android довольно простым. Если вам интересно посмотреть, как это сделать, я разместил весь исходный код на github .
Вот краткий обзор процедуры:
- Используйте AIR SDK для создания APK-файла AIR for Android.
- Используйте утилиту dex2jar для преобразования dex-классов AppEntry в файл JAR.
- Извлеките классы ресурсов из файла JAR, чтобы они не конфликтовали с новыми ресурсами.
- Используйте apktool, чтобы извлечь исходные ресурсы из APK-файла AIR for Android.
- Создайте один ZIP-файл, содержащий файл airbootstap.jar, ресурсы, файл AndroidManifest.xml и ресурсы.
Теперь вы можете просто скопировать и вставить эти зависимости в ваш проект Android.
Conclusion
Hopefully this article has helped you to better understand how you can extend AIR for Android applications with Android APIs. There are still a number of areas where this method can be improved. For instance, I am currently working with the Merapi Project developers to get Merapi working with my method of extending AIR for Android. That will provide a better bridging technique for communicating between the AIR application and Android APIs. So stay tuned for more information about that. And let me know if you have any questions!