Достаточно давно я ничего не писал в блог, поскольку у меня не было должного настроения для этого. Но всё же я собрался и в этом посте решил описать то, над чем я работал в своё свободное время с момента публикации последней статьи об изменении дизайна сайта. Так получилось, что здесь, в основном, будут описаны улучшения и обновления моих уже существующих проектов, а не процессы создания новых. Но тем не менее, новые приложения и их описания я вынесу в самое начало этого поста. Эти программы были созданы по просьбе моих старых друзей, они получились компактными и небольшими, поэтому посвящать им отдельные и развёрнутые статьи было бы немного странно.
Лаунчеры обновлённых приложений для Android OS, скриншоты с Motorola Photon Q.
Постепенно, для некоторых приложений, над которыми я работал, накопились важные обновления, исправления некоторых ошибок и другие патчи. Я решил описать все изменения тоже в этой статье. Таким образом, она будет представлять собой небольшой дайджест произошедшего примерно за четыре месяца и будет довольно объёмной и полной. Больше всего изменений получилось для моих приложений на Android OS, поэтому я вынесу информацию о них в конец этого поста. Кроме того, обновления затронули и некоторые другие проекты, об этом тоже будет написано подробнее. Но начнём с отдельных приложений.
Содержание:
1. Приложение Synergy Calls для Android OS
2. Демон keyd для MotoMAGX
3. Новые сборки Cave Story (NXEngine)
4. Обновление блога и темы Moto Juice
5. Обновление Telegram-бота Gadget Hackwrench (DigestBot)
6. Небольшой твик Bezier Clock для KDE Plasma 5
7. Обновление AstroSmash до версии 1.1
8. Обновление Ken’s Labyrinth до версии 1.1
9. Обновление Snooder 21 до версии 1.1
10. Обновление Spout до версии 1.1
11. Итог и заключение
1. Приложение Synergy Calls для Android OS
Мой знакомый Web-программист, использующий ник Synergy, попросил меня сделать небольшую программку для Android OS. Это приложение должно было отлавливать все входящие и исходящие звонки, собирать о них информацию, и отправлять всё это на указанный сервер посредством метода запроса POST. Днём я приступил к написанию программы и уже вечером получил первую рабочую версию приложения.
Интерфейс программы Synergy Calls, скриншот с Motorola Photon Q (превью, увеличение по клику).
После непродолжительных поисков в интернете, на ресурсе Stack Overflow за авторством Uma Kanth я нашёл отличный класс PhoneCallReceiver, который избавил меня от написания собственного велосипеда. Я протестировал этот класс и решил использовать его в качестве ядра программы.
Чтобы сервис приложения стартовал сразу после запуска смартфона, я отнаследовался от системного класса BroadcastReceiver и перегрузил метод onReceive() следующим образом:
1 2 3 4 5 6 7 8 |
public class BootReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Intent startServiceIntent = new Intent(context, SynergyService.class); context.startService(startServiceIntent); } } |
А для того, чтобы операционная система Android не выгружала со временем моё приложение из памяти устройства, я создал сервис SynergyService, отнаследовавшись от системного класса Service. Далее, в методе onCreate() я создаю экземпляр класса CallReceiver. Всё это выглядит приблизительно следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class SynergyService extends Service { public static final String LOG_TAG = "SynergyService"; private CallReceiver callReceiver = null; public void onCreate() { super.onCreate(); Log.d(LOG_TAG, "onCreate"); callReceiver = new CallReceiver(); SharedPreferences sharedPreferences = getSharedPreferences("ru.exlmoto.synergycall", MODE_PRIVATE); callReceiver.updateValues(sharedPreferences); } ... } |
Класс CallReceiver я унаследовал от упомянутого выше класса PhoneCallReceiver, перегрузив в нём соответствующие методы. Кроме того, в CallReceiver я создал вложенный класс PostClass, унаследованный от системного класса AsyncTask, который и занимается отправкой данных на сервер в виде POST-запроса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
public class CallReceiver extends PhoneCallReceiver { private class PostClass extends AsyncTask<String, Void, Void> { private final Context context; private final String a_url; private final String value; private final Date time; private final Date timeEnd; private final String time_s; private final String timeEnd_s; public PostClass(Context c, final String a_url, final String value, final Date time, final Date timeEnd) { this.context = c; this.a_url = a_url; this.value = value; this.time = time; this.timeEnd = timeEnd; DateFormat df = new SimpleDateFormat("dd-MM-yyyy_HH:mm:ss"); time_s = df.format(time); if (timeEnd != null) { timeEnd_s = df.format(timeEnd); } else { timeEnd_s = ""; } Log.d(APP_TAG, "POST: " + this.a_url + " " + this.value + " " + this.time + " " + this.timeEnd); } protected void onPreExecute() { } @Override protected Void doInBackground(String... params) { try { URL url = new URL(a_url); HttpURLConnection connection = (HttpURLConnection)url.openConnection(); String urlParameters = "phone=" + value + ";"; urlParameters += "time=" + time_s; if (timeEnd != null) { urlParameters += ";time_end=" + timeEnd_s; } connection.setRequestMethod("POST"); // connection.setRequestProperty("USER-AGENT", "Mozilla/5.0"); // connection.setRequestProperty("ACCEPT-LANGUAGE", "en-US,en;0.5"); connection.setDoOutput(true); DataOutputStream dStream = new DataOutputStream(connection.getOutputStream()); dStream.writeBytes(urlParameters); dStream.flush(); dStream.close(); int responseCode = connection.getResponseCode(); final StringBuilder output = new StringBuilder("Request URL " + url); output.append(System.getProperty("line.separator") + "Request Parameters " + urlParameters); output.append(System.getProperty("line.separator") + "Response Code " + responseCode); output.append(System.getProperty("line.separator") + "Type " + "POST"); BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream())); br.close(); } catch (MalformedURLException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } } public static final String APP_TAG = "CallReceiver"; public static String inUrl; public static String inEUrl; public static String outUrl; public static String outEUrl; public static String missUrl; @Override protected void onIncomingCallStarted(Context ctx, String number, Date start) { Log.d(APP_TAG, "Incoming: " + number); new PostClass(ctx, inUrl, number, start, null).execute(); } @Override protected void onOutgoingCallStarted(Context ctx, String number, Date start) { Log.d(APP_TAG, "Outgoing: " + number); new PostClass(ctx, outUrl, number, start, null).execute(); } @Override protected void onIncomingCallEnded(Context ctx, String number, Date start, Date end) { Log.d(APP_TAG, "Incoming end: " + number); new PostClass(ctx, inEUrl, number, start, end).execute(); } @Override protected void onOutgoingCallEnded(Context ctx, String number, Date start, Date end) { Log.d(APP_TAG, "Outgoing end: " + number); new PostClass(ctx, outEUrl, number, start, end).execute(); } @Override protected void onMissedCall(Context ctx, String number, Date start) { Log.d(APP_TAG, "Missed: " + number); new PostClass(ctx, missUrl, number, start, null).execute(); } public static void updateValues(SharedPreferences settingsStorage) { Log.d(APP_TAG, "Update Values from SharedPreferences"); inUrl = settingsStorage.getString("in", "http://exlmoto.ru"); inEUrl = settingsStorage.getString("inE", "http://exlmoto.ru"); outUrl = settingsStorage.getString("out", "http://exlmoto.ru"); outEUrl = settingsStorage.getString("outE", "http://exlmoto.ru"); missUrl = settingsStorage.getString("miss", "http://exlmoto.ru"); Log.d(APP_TAG, "inUrl: " + inUrl); Log.d(APP_TAG, "inEUrl: " + inEUrl); Log.d(APP_TAG, "outUrl: " + outUrl); Log.d(APP_TAG, "outEUrl: " + outEUrl); Log.d(APP_TAG, "missUrl: " + missUrl); } } |
Собственно, здесь всё просто. При поступлении любого вызова на смартфон, управление передаётся в перегруженные методы, в которых выполняется асинхронная задача, отправляющая подготовленный POST-запрос на сервер, который логирует всю поступающую информацию.
Пример лога на сервере, скриншот эмулятора терминала Konsole.
Для удобного управления опциями программы был создан простой GUI, позволяющий задавать нужные адреса для отправления POST-запросов и сохранять их. Функциональность интерфейса реализована в Activity-классе MainActivity, который помимо этого запускает сервис SynergyService, если он не был запущен:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); startService(new Intent(this, SynergyService.class)); settingStorage = getSharedPreferences("ru.exlmoto.synergycall", MODE_PRIVATE); // Check the first run // boolean firstRun = false; if (settingStorage.getBoolean("firstRun", true)) { // firstRun = true; settingStorage.edit().putBoolean("firstRun", false).commit(); } else { readSettings(); } initWidgets(); fillLayoutBySettings(); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { writeSettings(); Toast.makeText(v.getContext(), "Settings Saved", Toast.LENGTH_SHORT).show(); } }); } |
На Android OS версии 6.0 и выше, может потребоваться разрешить приложению Synergy Calls получать информацию о вызовах в настройках. Исходный код программы выложен на GitHub под лицензией MIT:
https://github.com/EXL/SynergyCalls
Скачать готовый APK-пакет можно по ссылке ниже:
[Скачать APK-пакет Synergy Calls, v1.0, 43 КБ | Download Synergy Calls APK-package, v1.0, 43 КB]
Надеюсь, это приложение пригодится и окажется полезным не только моему другу.
Update 05-MAY-2017: В проекте были обновлены компоненты Gradle, это ознаменовало выход новой версии Synergy Calls v1.1, скачать которую можно со странички проектов.
2. Демон keyd для MotoMAGX
Пользователь форума на ресурсе MotoFan.Ru, использующий ник fill.sa, попросил меня разобраться в том, почему на телефоне Motorola ZN5 не работает демон (программа, работающая в фоновом режиме) vr, который испаноговорящие разработчики из Аргентины написали специально для Motorola Z6. Эта программа используется для отлова нажатия на голосовую кнопку и для вызова соответствующей программы после этого действия.
Тестирование keyd на Motorola ZN5, скриншот Telnet-клиента PuTTYtel.
Поверхностное тестирование, которое провёл владелец ZN5 под ником VINRARUS, помогло мне выявить следующее: в канал QCopChannel("/hardkey/bc") и в некоторые другие протестированные каналы, посылаются данные только о нескольких нажатых кнопках во время работы аппарата, поэтому эта программа не работает должным образом на Motorola ZN5. Нужно было найти, во-первых, рабочее, а во-вторых, более кросс-платформенное решение проблемы отлова нажатий на кнопки. Спустя много часов поисков в Google, я нашёл примерный вариант решения на GitHub’е в проекте OPIE, о котором я уже упоминал.
Поскольку в MotoMAGX используется системная библиотека Qt/Embedded версии 2.3.8, то там есть собственный небольшой оконный сервер, называемый QWS. В Motorola решили пойти по пути наименьшего сопротивления и оставить его. Но тем не менее, в MotoMAGX сервер QWS немного изменён и выделен в отдельную программу, код которой закрыт, а называется она просто windowsserver. Но это неважно, так как мы можем отнаследоваться от класса ZApplication и перегрузить виртуальный метод qwsEventFilter(), в котором можно будет отлавливать информацию, которая передаётся через сервер. В данном случае нас интересуют только кнопки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
virtual bool qwsEventFilter(QWSEvent *event) { if (event->type == QWSEvent::Key) { QWSKeyEvent *keyEvent = static_cast<QWSKeyEvent *>(event); #ifdef DEBUG_LOG qDebug(QString("win: %1, unicode: %2, keycode: %3, modifier: %4, press: %5, repeat: %6") .arg(keyEvent->simpleData.window) .arg(keyEvent->simpleData.unicode) .arg(keyEvent->simpleData.keycode) .arg(keyEvent->simpleData.modifiers) .arg(keyEvent->simpleData.is_press) .arg(keyEvent->simpleData.is_auto_repeat)); #endif if (keyEvent->simpleData.is_press == 1 && keyEvent->simpleData.is_auto_repeat == 0) { if (keyEvent->simpleData.keycode != 65286) { vibThread->start(); } // Drop Camera Shutter catchButton(keyEvent->simpleData.keycode, keyEvent->simpleData.is_press); } } return true; } |
Здесь всё просто: фильтруем все события сервера и выбираем относящиеся к клавиатуре, затем приводим event к нужному типу и через структуру simpleData получаем доступ к необходимым и достаточно полным данным, вроде кода клавиши, статуса её удержания и прочей сопутствующей информацией, благодаря которой мы можем вызвать вибрацию в отдельном потоке, если это необходимо, а также обработать нажатие в методе catchButton():
1 2 3 4 5 6 7 8 |
void catchButton(uint keycode, uint is_press) { if (is_press) { QString system_call = config->find(keycode).data(); if (system_call) { system(QString("%1 %2").arg(system_call).arg(" &").ascii()); } } } |
Вызов системной программы осуществляется с помощью функции system(), а для того, чтобы управление после запуска программы снова вернулось в демон, в конце строки передаётся амперсанд.
Кстати, этот трюк с QWS был задокументирован лишь в Qt 3, в Qt 2 про него абсолютно ничего не говорится. Я соединил способ описанный выше со старым отловом нажатий кнопок через канал QCopChannel("/hardkey/bc") и получил то, что мне требовалось. Мониторинг нажатий через каналы реализуется ещё проще:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
void registerChannels() { if (QCopChannel::isRegistered("/hardkey/bc")) { bcChannel = new QCopChannel("/hardkey/bc", this); connect(bcChannel, // <- Throws event SIGNAL(received(const QCString &, const QByteArray &)), this, // <- Catch event SLOT(catchCoButton(const QCString &, const QByteArray &))); } } void catchCoButton(const QCString &message, const QByteArray &data) { QDataStream stream(data, IO_ReadOnly); if (message == "keyMsg(int,int)") { int key, type; stream >> key >> type; #ifdef DEBUG_LOG qDebug(QString("key: %1, type: %2").arg(key).arg(type)); #endif if (key == 65285) { // Gallery Key vibThread->start(); catchButton(key, type); } } } |
В классе Application есть методы registerChannels() и catchCoButton(). Первый вызывается в функции main() сразу после создания объекта, а второй является слотом, то есть вызывается сразу после сигнала получения события и отлавливает и обрабатывает нажатие на кнопку галереи.
Для настройки демона keyd я решил реализовать возможность чтения конфигурационного файла keyd.cfg следующего вида:
1 2 3 4 5 6 7 8 |
## Vibration # Syntax: <on/off>:<duration>, 0 - off, 1 - on, :x - duration in msec 1:150 ## Commands # Syntax: <keycode>:<shell command> 4144:/mmc/mmca1/my_script.sh 4145:/mmc/mmca1/my_executable |
Решётка в начале строки означает комментарий, а дальше идут опции, разделённые знаком двоеточия. В первой колонке указывается код требуемой кнопки, а во второй — путь до исполняемого файла или скрипта в системе, который нужно запустить по нажатию на неё. Для чтения значений из этого файла я создал специальный класс ShittyCfgParser:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
class ShittyCfgParser { int error; bool vibro; int vibrodur; QString cfgData; QMap<int, QString> configMap; void parseConfigData() { configMap.clear(); QStringList configList = QStringList::split('\n', cfgData); int count_config_lines = 0; for (uint i = 0; i < configList.count(); ++i) { QString configStr = configList[i]; if (configStr.startsWith("#")) { continue; } else if (configStr[0].isDigit()) { count_config_lines++; QStringList tokenizeLine = QStringList::split(':', configStr); if (tokenizeLine.count() == 2) { if (count_config_lines == 1) { // First element for vibration vibro = tokenizeLine[0].toInt(); vibrodur = tokenizeLine[1].toInt(); } else { configMap.insert(tokenizeLine[0].toInt(), tokenizeLine[1]); } } } } error = CONFIG_OK; } public: ShittyCfgParser(const QString &fileName) : error(CONFIG_ERROR) { readFileData(fileName); } ~ShittyCfgParser() { } int getError() const { return error; } bool getVibro() const { return vibro; } int getVibroDur() const { return vibrodur; } QMap<int, QString> *getConfigMap() const { return const_cast<QMap<int, QString> *>(&configMap); } void readFileData(const QString &fileName) { QFile configFile(fileName); if (configFile.exists()) { if (configFile.open(IO_ReadOnly)) { int size = configFile.size(); char data[size]; configFile.readBlock(data, sizeof(data)); cfgData = data; parseConfigData(); } else { qDebug(QString("Error opening file: %1.").arg(fileName)); } } else { qDebug(QString("Error: config file: %1 doesn't exist.").arg(fileName)); } configFile.close(); } }; |
После своего открытия файл считывается в массив data, который потом копируется в QString, после чего вызывается метод parseConfigData(), разбирающий данные и собирающий QMap, значения строк запуска приложений из которого можно получить по ключу-коду кнопки. Благодаря использованию конфигурационного файла появилась возможность настройки демона под свои нужды без перекомпиляции.
Этот проект является больше каркасом, скелетом или конструктором для последующего изменения, чем готовым для использования приложением. Например, VINRARUS создал на основе демона keyd приложение zBookmark, которое по своей функциональности напоминает метки в P2K-телефонах.
Скриншоты приложения zBookmark для Motorola ZN5.
Исходный код приложения keyd выложен на GitHub под лицензией Public Domain:
Скачать TAR.GZ-пакет с исполняемыми файлами для телефонов MotoMAGX можно по ссылке ниже:
[Скачать TAR.GZ-пакет keyd, v1.0, 35 КБ | Download keyd TAR.GZ-package, v1.0, 35 КB]
Приложение можно использовать по своему усмотрению, изменять или наращивать функциональность в нём. Кстати, таким трюком с QWS мне нужно будет попробовать отловить и другие типы событий оконного сервера, например, будет полезно поймать пересылаемую информацию в каналы, если это, конечно, возможно.
3. Новые сборки Cave Story (NXEngine)
Два года назад мне подарили Motorola ROKR E6 и я решил портировать замечательную игру Cave Story на движке NXEngine и на платформу EZX. Поскольку в SDK для этой платформы использовался очень старый компилятор GCC 3.3.6, то у меня возникли некоторые затруднения с выравниванием структур, так как sizeof(), применимый к ним, давал совершенно другой результат. Я сделал несколько исправлений в коде, чтобы исправить некоторые вылеты, но из-за нехватки свободного времени отложил этот порт и вспомнил о нём лишь недавно. Первое тестирование собранных пакетов дало положительные результаты, я без проблем смог пройти игру до уровня «Грасстаун» и дальше решил не продолжать прохождение. Поскольку собранные пакеты для платформы EZX оказались вполне рабочими, я решил опубликовать их.
Запущенная русская версия игры Cave Story на движке NXEngine на смартфоне Motorola ROKR E6; платформа EZX.
Администраторы фанатского сайта www.cavestory.org нашли в интернете мой проект по портированию NXEngine на разные устройства и добавили собранные мной пакеты для различных платформ на свой сайт, в раздел загрузок, за что им огромное спасибо. Позже со мной связался поклонник игры Cave Story — Ola Andersson, который попросил меня обновить английскую версию NXEngine под MS Windows (32-bit) до версии 1.0.0.6 и посодействовать продвижению форка NXEngine-evo на фанатский сайт, попутно исправив некоторые баги в нём. Все эти вопросы удалось решить и уладить, для этого я связался с iSage (автором NXEngine-evo), авторами www.cavestory.org и результатом нашей работы стали обновлённые и добавленные пакеты различных портов NXEngine на этом полуофициальном и популярном сайте поклонников Cave Story.
Более подробную информацию об этих событиях можно посмотреть в соответствующем разделе статьи про Cave Story (NXEngine), там же можно скачать и готовые для установки пакеты.
4. Обновление блога и темы Moto Juice
С момента обновления дизайна сайта я сделал несколько незначительных изменений и дополнений, опишу здесь лишь основные. Во-первых, на сайт была добавлена новая страница Projects, на которой будут публиковаться все основные и актуальные приложения, над которыми я работаю. Кроме того, виджет с пятью случайными проектами в шапке сайта получил дополнительную кнопку со стрелкой, которая тоже ведёт эту на страницу со списком пакетов приложений.
Стрелка в виджете случайных проектов, скриншот из браузера Chrome.
Моя статья EasyCAP USB 2.0 — Обзор и опыт использования за пять лет набрала очень большое количество комментариев, что привело к общему замедлению скорости загрузки страницы с обзором. Поэтому я решил включить в движке блога WordPress разбивку комментариев на страницы и заодно активировать более интересные аватары пользователей в формате Wavatar, раз уж скорость отображения после сокращения количества показываемых комментариев резко возросла. Примечательно, что новые аватары немного похожи на персонажей игры Snood™ 21.
Разбивка комментариев к статье на страницы и аватары пользователей формата Wavatar, скриншот из браузера Chrome.
Но мной была выявлена небольшая проблема: моя текущая тема MotoJuice, унаследованная от стандартного шаблона Twenty Fourteen, имела достаточно странную пагинацию комментариев без обозначения номера страницы. Пришлось вернуться на стандартную пагинацию следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
diff --git a/comments.php b/comments.php index 781c06d..a718841 100644 --- a/comments.php +++ b/comments.php @@ -32,8 +32,7 @@ if ( post_password_required() ) { <?php if ( get_comment_pages_count() > 1 && get_option( 'page_comments' ) ) : ?> <nav id="comment-nav-above" class="navigation comment-navigation" role="navigation"> <h1 class="screen-reader-text"><?php _e( 'Comment navigation', 'twentyfourteen' ); ?></h1> - <div class="nav-previous"><?php previous_comments_link( __( '← Older Comments', 'twentyfourteen' ) ); ?></div> - <div class="nav-next"><?php next_comments_link( __( 'Newer Comments →', 'twentyfourteen' ) ); ?></div> + <div class="paginate-comments"><?php paginate_comments_links(); ?></div> </nav><!-- #comment-nav-above --> <?php endif; // Check for comment navigation. ?> @@ -50,8 +49,7 @@ if ( post_password_required() ) { <?php if ( get_comment_pages_count() > 1 && get_option( 'page_comments' ) ) : ?> <nav id="comment-nav-below" class="navigation comment-navigation" role="navigation"> <h1 class="screen-reader-text"><?php _e( 'Comment navigation', 'twentyfourteen' ); ?></h1> - <div class="nav-previous"><?php previous_comments_link( __( '← Older Comments', 'twentyfourteen' ) ); ?></div> - <div class="nav-next"><?php next_comments_link( __( 'Newer Comments →', 'twentyfourteen' ) ); ?></div> + <div class="paginate-comments"><?php paginate_comments_links(); ?></div> </nav><!-- #comment-nav-below --> <?php endif; // Check for comment navigation. ?> |
И внести соответствующие поправки в стили CSS.
Ранее я занимался разработкой и поддержкой темы MotoJuice с помощью обычного редактора Kate, но недавно я успешно импортировал проект темы в интегрированную среду разработки NetBeans и сразу исправил достаточно серьёзную опечатку в стилях, которую нашёл мне встроенный парсер этой IDE. Из-за забытой запятой (на восьмой строке ниже) одно CSS-правило попросту игнорировалось браузерами.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
.entry-content a:hover, .entry-summary a:hover, .page-content a:hover, .comment-content a:hover { text-decoration: underline; } , .entry-content a.button, .entry-summary a.button, .page-content a.button, .comment-content a.button { text-decoration: none; } |
Одно из важных преимуществ IDE состоит в том, что они помогают бороться с такими вот опечатками и невнимательностью программиста.
5. Обновление Telegram-бота Gadget Hackwrench (DigestBot)
С момента создания дайджест-бота Gadget Hackwrench прошло почти два года и он до сих пор работает и обслуживает нашу группу в Telegram. Кроме того, функциональность программы благодаря стараниям моего друга Zorge.R сильно увеличилась, например, добавилась возможность вывода графиков котировок валют и металлов, отправка стикеров по ID, мониторинг игровых серверов и множество других полезных функций.
Профиль дайджест-бота Gadget Hackwrench, скриншот из мессенджера Telegram.
Кроме того, к боту была прикреплена простейшая система сохранения дайджест-лога с поддержкой ротации, которая поможет восстановить данные после какого-нибудь факапа. Это уже помимо того, что каждое сообщение с тегом #digest пишется на диск сервера. Итак, основную работу выполняет небольшой Bash-скрипт:
1 2 3 4 5 6 7 |
#!/bin/bash DAYMONTH=`date +"%Y%m%d_%H%M%S"` /bin/tar -czf /root/digestbot/BackUpDigestLog/backup_digest_log_$DAYMONTH.tar.gz -C /root/digestbot/DigestBot/ DigestBotStackLog.json # Backup rotation, default 100 files FILES=100 ls -1 /root/digestbot/BackUpDigestLog/backup_* | sort -r | tail -n +$(($FILES+1)) | xargs rm > /dev/null 2>&1 |
При выполнении скрипт производит упаковку истории в архив с именем, в котором содержится дата и время. Затем скрипт проверяет количество файлов в директории, если их количество больше числа в переменной FILES, то он удаляет все старые файлы, выходящие за верхний предел этого числа. Это называется ротацией логов.
Сохранённые резервные копии истории дайджест-бота Gadget Hackwrench, скриншот из эмулятора терминала Konsole.
Чтобы этот скрипт отрабатывал в нужное время, используется функциональность системы инициализации systemd, для которой был создан юнит digestbotbackup.service:
1 2 3 4 5 6 7 8 9 |
[Unit] Description=Backup of DigestBot Stack Log [Service] Type=simple ExecStart=/root/backup_digest_log.sh [Install] WantedBy=multi-user.target |
И специальный таймер digestbotbackup.timer:
1 2 3 4 5 6 7 8 9 |
[Unit] Description=Timer for DigestBot Log Stack BackUp [Timer] OnCalendar=*-*-* 22:00:00 Unit=digestbotbackup.service [Install] WantedBy=multi-user.target |
Благодаря которому сервис вызывает скрипт один раз в сутки, в десять часов вечера. Информации о юнитах и таймерах в systemd в интернете полно и поиск нужного руководства по их настройке и использованию не вызывает сложностей.
Моя давняя статья про написание простейших ботов для Telegram на JavaScript и Python за это время немного потеряла в своей актуальности, но базовые принципы создания подобных ботов для мессенджера Telegram там всё ещё описаны достаточно подробно. Если этого недостаточно, то исходный код текущей реализации дайджест-бота можно посмотреть на GitHub’е, а скачать готовый пакет для Node.js можно по этой ссылке:
[Скачать ZIP-пакет DigestBot, v1.0.6, 337 КБ | Download DigestBot ZIP-package, v1.0.6, 337 КB]
[Скачать ZIP-пакет DigestBot, v1.0.5, 309 КБ | Download DigestBot ZIP-package, v1.0.5, 309 КB]
[Скачать ZIP-пакет DigestBot, v1.0.4, 204 КБ | Download DigestBot ZIP-package, v1.0.4, 204 КB]
Руководство по запуску, установке и развертыванию в системе этого бота опубликовано в README.md файле на GitHub’е.
6. Небольшой твик Bezier Clock для KDE Plasma 5
Пользователь чата MotoFan.Ru в Telegram, использующий ник J()KER, попросил меня немного поправить часы на кривых Безье для рабочего стола KDE Plasma 5, создание которых я описывал в этой статье. Суть исправления заключалась в возможности установки собственного изображения в качестве фонового. Я решил предоставить ему небольшой концепт подобного обновления, который сделал буквально за минуту.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
diff --git a/qml/BezierCanvas.qml b/qml/BezierCanvas.qml index 5e68cfa..294accd 100644 --- a/qml/BezierCanvas.qml +++ b/qml/BezierCanvas.qml @@ -43,7 +43,8 @@ Canvas { context.save(); // Fill Background - CanvasFunctions.fillCanvasByColor(context, setup.backgroundColor); + // CanvasFunctions.fillCanvasByColor(context, setup.backgroundColor); + context.clearRect(0, 0, canvas.width, canvas.height); // Translate Context CanvasFunctions.translateContex(context); diff --git a/qml/BezierClock.qml b/qml/BezierClock.qml index f6f748d..35d0545 100644 --- a/qml/BezierClock.qml +++ b/qml/BezierClock.qml @@ -28,6 +28,8 @@ Rectangle { width: 2300 * setup.visualScaling height: 600 * setup.visualScaling + color: "transparent" + BezierCanvas { id: canvas anchors.fill: parent diff --git a/qml/main.qml b/qml/main.qml index cec95b5..eff7841 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -24,11 +24,12 @@ import QtQuick 2.0 -Rectangle { +import org.kde.plasma.core 2.0 as Plasmacore + +Image { id: root - width: 800 - height: 480 - color: setup.backgroundColor + fillMode: wallpaper.configuration.FillMode + source: "http://wstaw.org/m/2017/03/02/wall_1.png" BezierSettings { id: settings |
Суть изменений заключается в следующем: в файле main.qml корневой элемент изменяется с Rectangle на Image, соответственно, изменяется и набор основных свойств, например, в свойстве source можно задать путь к изображению, которое может быть расположено как в сети, так и на диске компьютера. Далее элементу BezierClock устанавливается прозрачность, а в элементе BezierCanvas заливка холста белым цветом изменяется на простую очистку куска экрана.
Твик установки собственного фонового изображения для часов Bezier Clock, скриншот с Arch Linux и KDE Plasma 5 (превью, увеличение по клику).
К сожалению, в KDE Plasma 5 так и не исправили баги под номерами #367546 и #366390, о которых я говорил в соответствующей статье. Баги недавно были помечены как решённые, но на деле абсолютно ничего не изменилось и настройки живых обоев при переключении так и не сохраняются. Поэтому сейчас у меня нет особого настроения доделывать до ума этот твик с установкой собственных фоновых изображений до ума. Конечно, было бы неплохо сделать возможность выбора фона из GUI с настройками, а не путём редактирования свойства source в файле main.qml, но пока это останется так.
Описанные изменения Bezier Clock я выложил в ветку image_test в свой репозиторий Bezier Clock на GitHub’е. Скачать TAR.XZ-пакет для пакетного менеджера Plasma Shell можно по этой ссылке:
Установить его можно с помощью команды plasmapkg2 -t wallpaperplugin -i bezier-clock-v1.0.tar.xz, а команда plasmapkg2 -t wallpaperplugin -r ru.exlmoto.bezierclock произведёт удаление пакета.
7. Обновление AstroSmash до версии 1.1
После моей публикации игры AstroSmash для Android OS на GitHub’е, со мной связался её главный разработчик Albert So и мы с ним очень мило пообщались. Он рассказал мне о создании компании Lavastorm, в которой он трудился на тот момент главным программистом, о том как лопнул «пузырь доткомов» и о том, как сложно было в 2001 году разрабатывать мобильные игры для Motorola T720 по причине нестабильной работы программной части и, к тому же, в рамках весьма аскетичных технических характеристик.
За это время накопились только незначительные улучшения игры, которые я отмечу ниже. Я решил немного оптимизировать отрисовку сенсорной стрелки на канвасе, и рисовать её только один раз при запуске игры:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
diff --git a/src/ru/exlmoto/astrosmash/AstroSmashView.java b/src/ru/exlmoto/astrosmash/AstroSmashView.java index cbef5f5..aebe22a 100644 --- a/src/ru/exlmoto/astrosmash/AstroSmashView.java +++ b/src/ru/exlmoto/astrosmash/AstroSmashView.java @@ -72,6 +72,8 @@ implements SurfaceHolder.Callback, IGameWorldListener, Runnable { private Canvas bitmapCanvas = null; private Canvas globalCanvas = null; private Bitmap gameScreen = null; + private Bitmap touchArrowBitmap = null; + private Canvas touchArrowCanvas = null; private Rect touchRect = null; private Rect bitmapRect = null; @@ -163,7 +165,9 @@ implements SurfaceHolder.Callback, IGameWorldListener, Runnable { bitmapRect, screenRectPercent, painter); - drawTouchArrow(canvas, painter); + canvas.drawBitmap(touchArrowBitmap, + 0, screenRectChunkProcent, + painter); } else { canvas.drawBitmap(gameScreen, bitmapRect, @@ -181,7 +185,7 @@ implements SurfaceHolder.Callback, IGameWorldListener, Runnable { private void drawTouchArrow(Canvas canvas, Paint paint) { paint.setColor(Version.GREENCOLOR_DARK); - canvas.drawRect(0, screenRectChunkProcent, screenWidth, screenHeight, paint); + canvas.drawRect(0, 0, screenWidth, screenHeightPercent, paint); paint.setStrokeCap(Cap.ROUND); paint.setAntiAlias(true); for (int i = 0; i < 2; ++i) { @@ -335,14 +339,19 @@ implements SurfaceHolder.Callback, IGameWorldListener, Runnable { px25 = px(25); px25double = px25 * 2; - arrow_Y0 = screenHeight - touchRect.height() / 2; - arrow_Y1 = screenHeight - touchRect.height() / 4; - arrow_Y2 = screenHeight - touchRect.height() / 4 * 3; + arrow_Y0 = touchRect.height() / 2; + arrow_Y1 = touchRect.height() / 4; + arrow_Y2 = touchRect.height() / 4 * 3; arrow_X0 = screenWidth - px25; arrow_X1 = screenWidth - px25double; screenRectChunkProcent = screenHeight - screenHeightPercent; + + touchArrowBitmap = Bitmap.createBitmap(screenWidth, screenHeightPercent, Bitmap.Config.ARGB_8888); + touchArrowCanvas = new Canvas(touchArrowBitmap); + + drawTouchArrow(touchArrowCanvas, painter); } @Override |
История изменений v1.1:
- Исправлена ошибка, связанная с невозможностью воспроизведения звука после его выключения в лаунчере.
- Проведён рефакторинг кода, отвечающего за отображение очков в лаунчере.
- Исправлены диалоги.
- Добавлены некоторые улучшения в лаунчере.
- Изменён и оптимизирован алгоритм отрисовки управляющей стрелки: теперь она рисуется только один раз при запуске игры.
- Изменены рекорды.
- Количество жизней теперь ограничено девятью, как это было на Motorola C350 и Motorola V150.
- В движок возвращены некоторые try/catch обёртки, так как было поймано несколько вылетов за границы массивов.
Известные проблемы v1.1:
- Иногда при попадании лазера в бомбу она не разрушается, а телепортируется наверх и снова начинает падать с большой скоростью вниз. Этот баг где-то далеко в недрах декомпилированного движка, видимо в коде просчёта коллизий, поэтому я так и не могу до него добраться.
- Поскольку игра использует системный класс SurfaceView, отрисовка выполняется средствами CPU и может быть медленной на высоких разрешениях и слабых процессорах, если используется сглаживание.
Скачать готовый APK-пакет игры AstroSmash v1.1 для Android OS, можно по ссылке ниже:
[Скачать APK-пакет AstroSmash, v1.1, 111 КБ | Download AstroSmash APK-package, v1.1, 111 КB]
Все изменения и пакеты опубликованы в моём репозитории игры AstroSmash на GitHub. Ещё раз хочу выразить огромную благодарность Albert So за его отзывчивость и доброжелательность, я был очень рад получить такой фидбэк.
8. Обновление Ken’s Labyrinth до версии 1.1
Наименьшее количество изменений за это время получил мой порт игры Ken’s Labyrinth на Android OS. Из самого интересного это исправление ошибки, которую я допустил по невнимательности и забывчивости:
1 2 3 4 5 6 7 8 9 10 11 12 |
diff --git a/src/ru/exlmoto/kenlab3d/KenLab3DTouchButtonsRects.java b/src/ru/exlmoto/kenlab3d/KenLab3DTouchButtonsRects.java index 281102a..73d2618 100644 --- a/src/ru/exlmoto/kenlab3d/KenLab3DTouchButtonsRects.java +++ b/src/ru/exlmoto/kenlab3d/KenLab3DTouchButtonsRects.java @@ -85,6 +85,8 @@ public class KenLab3DTouchButtonsRects { public void release() { m_buttonPushed = false; + m_buttonTouchId = -1; + SDLActivity.onNativeKeyUp(m_buttonCode); } |
Из-за того, что я ранее не сбрасывал значение переменной m_buttonTouchId, в которой сохраняется ID пальца отпущенной сенсорной кнопки, сенсорные элементы управления работали неправильно и коряво. Например, некоторые клавиши залипали, когда на сенсорных кнопках было несколько пальцев. Почему-то на поиск способа исправления этой проблемы я потратил достаточно много времени, хотя решение лежало на самой поверхности.
История изменений v1.1:
- Обновлён контент в репозитории.
- Исправлены диалоги.
- Добавлены некоторые улучшения в лаунчере.
- Исправлена проблема с отжатием нескольких сенсорных кнопок.
- Поправлена прозрачность кнопок сенсорного управления.
- Уменьшен размер SDL2-библиотеки.
Известные проблемы v1.1:
- Игровые меню рисуются через подпорки и костыли, подставленные из-за невозможности отрисовки методом «грязных прямоугольников» на ускорителях Adreno GPU и некоторых других, а потому в них отсутствует задний фон.
Скачать готовый APK-пакет игры Ken’s Labyrinth v1.1 для Android OS, можно по ссылке ниже:
Все изменения и пакеты опубликованы в моём репозитории порта Ken’s Labyrinth на GitHub. Я буду очень рад, если создатель игры Ken Silverman обратит внимание на мой небольшой труд.
9. Обновление Snooder 21 до версии 1.1
Незначительные изменения затронули и мой ремейк популярного карточного пасьянса, который я назвал Snooder 21. Была исправлена опасная ошибка, опять же, допущенная по невнимательности. Из-за неё при выходе из игры в некоторых случаях был вылет на слабых и старых Android-устройствах:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
diff --git a/snooder21/src/main/java/ru/exlmoto/snood21/SnoodsSurfaceView.java b/snooder21/src/main/java/ru/exlmoto/snood21/SnoodsSurfaceView.java index a477c27..36be268 100644 --- a/snooder21/src/main/java/ru/exlmoto/snood21/SnoodsSurfaceView.java +++ b/snooder21/src/main/java/ru/exlmoto/snood21/SnoodsSurfaceView.java @@ -1023,9 +1023,6 @@ public class SnoodsSurfaceView extends SurfaceView @Override public void surfaceDestroyed(SurfaceHolder holder) { SnoodsGameActivity.toDebug("Surface destroyed."); - resetGame(true); - clearColumns(); boolean shutdown = false; mIsRunning = false; while (!shutdown) { @@ -1038,6 +1035,8 @@ public class SnoodsSurfaceView extends SurfaceView SnoodsGameActivity.toDebug("Error joining to Main Thread"); } } + resetGame(true); + clearColumns(); } |
Проблема решилась простым перемещением очищающих функций resetGame() и clearColumns() в конец логического блока. Изначально игровые поля сначала очищались, а потом уже совершался выход из потока, что приводило к выбросу фатального исключения из-за невозможности обращения к каким-либо элементам игровых массивов. Вылет было сложно отловить, поскольку на целых 20 запусков Snooder 21, исключение выбрасывалось лишь 1-2 раза.
История изменений v1.1:
- Исправлены диалоги.
- Добавлены некоторые улучшения в лаунчере.
- Исправлен выброс фатального исключения после разрушения экземпляра класса SurfaceView.
- Обновлен gradle-wrapper.
- Изменены рекорды пользователей.
Известные проблемы v1.1:
- Игра была неправильно спроектирована, поэтому скорость некоторых анимаций зависит от производительности CPU.
- Поскольку игра использует системный класс SurfaceView, отрисовка выполняется средствами CPU и может быть медленной на высоких разрешениях и слабых процессорах, если используется сглаживание.
- После сворачивания игры в фон и разворачивания она начинается заново, а прогресс теряется.
- Иногда на третьей колонке сброшенных карт не воспроизводится звук и вибрация.
Скачать готовый APK-пакет игры Snooder 21 v1.1 для Android OS, можно по ссылке ниже:
[Скачать APK-пакет Snooder 21, v1.1, 421 КБ | Download Snooder 21 APK-package, v1.1, 421 KB]
Все изменения и пакеты опубликованы в моём репозитории игры Snooder 21 на GitHub. К сожалению, код моего ремейка очень ужасен и требует полного переписывания, но я никак не смог выделить достаточное количество своего свободного времени для этого. А недавно со мной связался поклонник подобных игр, использующий ник Smurf 5, который предложил мне написать онлайн-версию любимой игры, чтобы у него была возможность играть в неё на любой платформе. Меня весьма заинтересовало его предложение, так как я всегда хотел попробовать создать какое-нибудь популярное Web-приложение. Теперь думаю, какие инструменты могут помочь мне решить эту задачу, пока выбор рассматривается между Pixi.js и LibGDX. Надеюсь, при работе над этим проектом я не повторю допущенные ранее ошибки проектирования.
10. Обновление Spout до версии 1.1
Больше всего изменений за это время пришлось на мой порт игры Spout для Android OS. В новой версии был полностью переписан лаунчер игры. Старое управление, сделанное с помощью монструозных и ужасных родных сенсорных элементов Android OS, было заменено на две новых реализации. Кроме того, было исправлено огромное количество ошибок, некоторые из которых приводили даже к вылетам из игры. Первый вариант сенсорного управления с помощью полупрозрачных кнопок был реализован по тому же принципу, что и в моём порте Ken’s Labyrinth:
Вариант управления сенсорными кнопками, скриншот с Motorola Photon Q (превью, увеличение по клику).
Освещать подробности его работы я не считаю необходимым. А вот второй вариант, с помощью сенсорного колеса-джойстика, несколько интереснее и удобнее, поэтому в игре он активирован по-умолчанию.
Вариант управления сенсорным джойстиком, скриншот с Motorola Photon Q (превью, увеличение по клику).
Его реализацию я подсмотрел в исходных кодах программиста, который использует никнейм [SoD]Thor. Я смог интегрировать такой джойстик в свою игру. Более подробное воплощение подобного варианта управления уже в коде, я опишу в своей следующей статье, посвящённой портированию игры Adamant Armor Affection Adventure на Android OS.
Из-за невнимательного чтения документации я забывал удалять ссылки на JNI-объекты после их использования, поэтому при создании огромного числа ссылок (например, при частых вызовах кода вибрации) я получал примерно такую ошибку:
1 |
dalvikvm(11625): Failed adding to JNI local ref table (has 512 entries) |
И последующий вылет из игры. Проблема решилась добавлением вызова функции DeleteLocalRef() для утилизации созданных ссылок:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
diff --git a/jni/Spout/src/piece.c b/jni/Spout/src/piece.c index 887217b..fcc52d0 100644 --- a/jni/Spout/src/piece.c +++ b/jni/Spout/src/piece.c @@ -813,6 +813,8 @@ void pceFileWriteSct (const void *ptr, /*int sct,*/ int len) // Call JAVA-method (*javaEnviron)->CallStaticVoidMethod(javaEnviron, clazz, methodId, hiScore[1], hiScore[0]); + + (*javaEnviron)->DeleteLocalRef(javaEnviron, clazz); } } #endif // !ANDROID NDK @@ -887,6 +889,7 @@ void vibrateFromJNI(int duration) { // Call JAVA-method (*javaEnviron)->CallStaticVoidMethod(javaEnviron, clazz, methodId, duration); + (*javaEnviron)->DeleteLocalRef(javaEnviron, clazz); } } } @@ -910,6 +913,8 @@ void playGameOverSoundFromJNI() { jint soundID = (*javaEnviron)->GetStaticIntField(javaEnviron, clazz, fieldID); LOGI("JNI: soundID is: %d", (int)soundID); + (*javaEnviron)->DeleteLocalRef(javaEnviron, clazz); + // Now return to SpoutActivity clazz = (*javaEnviron)->FindClass(javaEnviron, "ru/exlmoto/spout/SpoutActivity"); if (clazz == 0) { @@ -926,6 +931,8 @@ void playGameOverSoundFromJNI() { // Call JAVA-method (*javaEnviron)->CallStaticVoidMethod(javaEnviron, clazz, methodId, soundID); } + + (*javaEnviron)->DeleteLocalRef(javaEnviron, clazz); } } } |
Для того, чтобы лаунчер не закрывался после вызова статического метода System.exit(), мне пришлось использовать вот такую подпорку:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
diff --git a/src/ru/exlmoto/spout/SpoutActivity.java b/src/ru/exlmoto/spout/SpoutActivity.java index 4b2f749..aa624bc 100644 --- a/src/ru/exlmoto/spout/SpoutActivity.java +++ b/src/ru/exlmoto/spout/SpoutActivity.java @@ -28,6 +28,7 @@ import java.io.IOException; import android.app.Activity; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.hardware.Sensor; import android.hardware.SensorEvent; @@ -239,6 +240,12 @@ public class SpoutActivity extends Activity implements SensorEventListener { } } + private void startLauncher() { + Intent intent = this.getPackageManager() + .getLaunchIntentForPackage("ru.exlmoto.spout"); + startActivity(intent); + } + @Override protected void onDestroy() { toDebug("Destroying activity..."); @@ -251,13 +258,15 @@ public class SpoutActivity extends Activity implements SensorEventListener { public void onBackPressed() { toDebug("Back key pressed!, Exiting..."); - m_spoutNativeSurface.onPause(); - m_spoutNativeSurface.onClose(); - if (SpoutSettings.s_Sound) { soundPool.release(); } + startLauncher(); + + m_spoutNativeSurface.onPause(); + m_spoutNativeSurface.onClose(); + // Because we want drop all memory of library // System.exit(0); // Now exit() call in native deinitSpoutGLES() diff --git a/src/ru/exlmoto/spout/SpoutLauncher.java b/src/ru/exlmoto/spout/SpoutLauncher.java index 9f66c52..0c85395 100644 --- a/src/ru/exlmoto/spout/SpoutLauncher.java +++ b/src/ru/exlmoto/spout/SpoutLauncher.java @@ -498,6 +498,7 @@ public class SpoutLauncher extends Activity { if (testOffsets(SpoutSettings.s_OffsetX, SpoutSettings.s_OffsetY, displayW, displayH)) { Intent intent = new Intent(v.getContext(), SpoutActivity.class); startActivity(intent); + finish(); } else { showDialog(OFFSET_ERROR); } |
Вызов System.exit() (или exit(), если речь идёт о коде на языках C/C++) необходим, поскольку без него после выхода и повторного входа в игру появляются различные баги из-за того, что нативная библиотека не может заново нормально переинициализироваться.
История изменений v1.1:
- Обновлён контент в репозитории.
- Исправлен вылет из-за большого количества неудалённых ссылок в нативном коде (JNI).
- Исправлен выброс исключения при парсинге целочисленного параметра.
- Добавлена регулировка громкости звуковых эффектов.
- Добавлены диалоги.
- Добавлена новая версия лаунчера с обложкой.
- Добавлено управление сенсорным джойстиком (спасибо [SoD]Thor‘у).
- Добавлено управление сенсорными кнопками.
- Убрано управление стандартными элементами Android OS.
- Исправлен запуск лаунчера после выхода из игры.
Известные проблемы v1.1:
- На слабых и старых Android-устройствах при активации сенсорного управления в игре может проседать FPS.
- На нескольких кастомных Android-лаунчерах и в некоторых других ситуациях нажатие на кнопку «Back» во время игры приведёт к вылету, а не к возврату в лаунчер Spout.
Скачать готовый APK-пакет игры Spout v1.1 для Android OS, можно по ссылке ниже:
[Скачать APK-пакет Spout, v1.1, 207 КБ | Download Spout APK-package, v1.1, 207 KB]
Все изменения и пакеты опубликованы в моём репозитории порта игры Spout на GitHub. Кстати, в лаунчер игры было добавлено небольшое изображение-обложка, в соответствии с остальными моими проектами для Android OS. Теперь Spout совершенно не выбивается из ряда моих других приложений.
11. Итог и заключение
Итак, в прошедшие четыре месяца я занимался в основном обновлением уже существующих проектов или созданием небольших программ по просьбам моих друзей. Все мои наработки вы можете увидеть в соответствующих репозиториях. Скачать готовые пакеты можно либо по ссылкам в этой статье, либо на новой странице блога, которая посвящена значимым и актуальным проектам. Кроме того, некоторые пакеты опубликованы в релизах самих репозиториев.
Надеюсь, кому-нибудь описанный здесь мой опыт решения различных проблем и ошибок когда-нибудь сможет пригодиться.
А Antony So — это девелопер астросмеша что ли?
Albert. Да. Вообще создателем оригинальной игры является John Sohl, он разрабатывал эту игру для IntelliVision. А потом Albert So разрабатывал J2ME-версию этой игры для Motorola T720, а позже и для Motorola V150, Motorola C350 и некоторых других телефонов как от Motorola, так и от Sanyo и LG. В декомпилированном исходном коде игры это довольно очевидно прослеживается, например, здесь. Историю игры AstroSmash я довольно подробно описал здесь.