Продолжение той самой истории.
Первая часть тут, вторая тут, третья тут.
4,5 года назад я имел неосторожность начать писать свою криптовалюту на совсем неподходящем для этого дела языке — на PHP. В итоге, конечно, написал (я упрямый), но получился костыль на костыле и то, что оно вообще работало было просто какой-то магией.
Сразу хочу предупредить, программер я самоучка-недоучка и пишу код, мягко сказать, неидеально.
Началось всё с того, что я расстался с девушкой, по имени Катя и в этот же день (4 апреля 2015-го) решил изучить Go и переписать свою криптовалюту. Писать про Катю не под спойлерами не могу, т.к. хабр всё же для IT-шных статей, а не для любовных рассказов и суровые айтишники, которым интересна тема Go, могут просто не обращать внимание на спойлеры «про Катю».
Итог 8 месяцев: приложение работает на Win (64/32), OSX(64/32), Linux(64/32), FreeBSD(64/32), Android, IOS.
Общего кода ~73к строк, кода под разные ОС где-то несколько сотен строчек.
40к — обработка/генерация блоков/тр-ий, 17.5к — контроллеры для интерфейса, 15.5к — шаблоны
Поддерживаются PostgreSQL, SQLite, MySQL.
Тех, кто будет тестировать мое творение, предупреждаю — могут быть баги, и если у Вас есть время, черкните о них, пожалуйста, на darwin@dcoin.club или в личку на хабре. Пожелания и советы тоже приветствуются.
В первых трех частях я рассказал про то, как в dcoin функционирует веб-сервер, про html/template, базы данных, плавное завершение приложения, шифрование и парсинг блоков.
В этой статье я расскажу про работу с Go на Android.
Начало
Смешно сказать, но мой первый андройд появился у меня этим летом. До этого я просто не находил причин, чтобы заменить свою Nokia 1200. Купил дешевый ZTE за 3000 руб с 512 памяти и 2-я ядрами. Для тестирования самое то. И звонить с него тоже можно
Хотелось сделать компиляцию через github.com/golang/mobile в apk. Посмотрел мануалы, вроде всё просто. Почти сразу получилось скомпилировать бинарник и запустить его под рутом на андройде. Обрадовался, что всё идет как по маслу и казалось, что через пару дней у меня будет apk, запустив который я увижу Dcoin.
Собрать Apk оказалось не сложно. В принципе, всё что мне было нужно — это автоматически открыть в браузере 127.0.0.1:8089. Вот тут-то я забуксовал. Несколько дней гуглил и экспериментировал и всё чего смог добиться — это отрисовка картинки на которой я прошу пользователя открыть в браузере нужный хост.
Решил зайти через aar. Т.е. добавить его как библиотеку в андройд-студио. И средствами студии уже открыть браузер или webview. Но sqlite упорно не хотело компилиться, оказалось что ошибка в компиляторе C и решения проблемы на тот момент не было (сейчас, кстати, уже есть).
Через пару дней написал ей, что на несколько месяцев ухожу с головой в свой проект и попросил не беспокоить меня по пустякам.
Через неделю от Кати пришла смс-ка «привет. ну как ты там?». Я не ответил. Через неделю еще одна «привет. как дела?». Я снова не ответил.
GoNativeActivity
В какой-то момент стало очевидно, что без этого волшебного файлика ничего не выйдет. Начал экспериментировать, внес несколько изменений, генерирую apk и ничего не меняется. Через пару дней не выдержал и решил написать одному из разработчиков gomobile, ответ пришел довольно быстро. Оказывается, после изменений в GoNativeActivity надо вызывать go generate github.com/c-darwin/mobile/cmd/gomobile, чтобы сгенерировался .dex файл и только после этого go install github.com/c-darwin/mobile/cmd/gomobile
Научившись править GoNativeActivity я получил огромные возможности. Нужно было лишь уметь писать на Java. А я не умел, и сейчас не умею. Но кое-что всё же смог сделать. Тут мой GoNativeActivity. Чуть позже понял, как создать свой AndroidManifest.xml, что дало еще больше возможностей, в итоге вместо работы в браузере я смог добиться работы в WebView, тут моя реализация вебвьюхи.
Уведомления
Мне захотелось сделать уведомления, когда приходят деньги или поступает входящий запрос на обмен монет на фиат
После гугления и изучения stackoverflow получился такой код
public void notif(String title, String text) {
Intent intent = new Intent("org.golang.app.MainActivity");
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this);
// с этой иконкой пришлось повозиться, нижу расскажу
mBuilder.setSmallIcon(R.drawable.icon);
mBuilder.setContentTitle(title);
mBuilder.setContentText(text);
Intent resultIntent = new Intent(this, MainActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(MainActivity.class);
// Adds the Intent that starts the Activity to the top of the stack
stackBuilder.addNextIntent(resultIntent);
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT);
mBuilder.setContentIntent(resultPendingIntent);
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// notificationID allows you to update the notification later on.
mNotificationManager.notify(2, mBuilder.build());
}
Осталось только понять, как этот notif дергать из Go. Вот тут я застрял еще на недельку. Оказалось, что надо использовать некого зверя под названием JNI. Получается что-то вроде такой конструкции: Go вызывает C, который запускает Java-машину и дергает через неё мой notif. Ужас. Особенно учитывая, что в C, я также как и в Java, почти полный ноль.
Короче, после долгих мучений я смог таки написать рабочий код и даже понять, что в нем происходит:
package notif
/*
#cgo LDFLAGS: -llog -landroid
#include <android/log.h>
#include <jni.h>
#include <stdlib.h>
#define LOG_FATAL(...) __android_log_print(ANDROID_LOG_FATAL, "Go/fatal", __VA_ARGS__)
void notif_manager_init(void* java_vm, void* ctx, char* title, char* text) {
JavaVM* vm = (JavaVM*)(java_vm);
JNIEnv* env;
int err;
int attached = 0;
err = (*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6);
if (err != JNI_OK) {
if (err == JNI_EDETACHED) {
// присоединяемся к JM
if ((*vm)->AttachCurrentThread(vm, &env, 0) != 0) {
LOG_FATAL("cannot attach JVM");
}
attached = 1;
} else {
LOG_FATAL("GetEnv unexpected error: %d", err);
}
}
// преобразуем в jstring наш title
jstring javaTitle = (jstring)(*env)->NewStringUTF(env, (const char *)title);
// преобразуем в jstring наш text
jstring javaText = (jstring)(*env)->NewStringUTF(env, (const char *)text);
//указатель на класс, к которому относится объект
jclass cls = (*env)->GetObjectClass(env, ctx);
// идентификатор метода.Ljava/lang/String;Ljava/lang/String - тип передаваемых данных и V(Void) - возвращаемых
jmethodID nJmethodID = (*env)->GetMethodID(env, cls, "notif", "(Ljava/lang/String;Ljava/lang/String;)V");
// вызываем сам метод
(jstring)(*env)->CallObjectMethod(env, ctx, nJmethodID, javaTitle, javaText);
if (attached) {
// отсоединяемся от JM
(*vm)->DetachCurrentThread(vm);
}
}
*/
import "C"
import (
"github.com/c-darwin/mobile/internal/mobileinit"
)
func SendNotif(title string, text string) {
ctx := mobileinit.Context{}
C.notif_manager_init(ctx.JavaVM(), ctx.AndroidContext(), C.CString(title), C.CString(text))
}
По аналогии у меня получился пакет get_files_dir.go который получает рабочую директорию.
Когда провожал её до такси спросил:
— Мы ведь не вместе да? Т.е. я могу делать что захочу?
Она: — В смысле «что захочу»? нет, мы вместе (пододвигается ко мне)
Я: — Ну у нас же не было первого свидания, мы еще не начали всё с начала.
Она: — Так вот было же только что.
Я: — Какое же это свидание, ты просто за деньгами приехала.
Мы уже стояли у дверей такси, я её усадил, расплатился с водителем, она уехала.
На Go получился вот такой код для вызова уведомления
// +build android
package sendnotif
import (
"github.com/c-darwin/mobile/notif"
)
func SendMobileNotification(title, text string) {
notif.SendNotif(title, text)
}
Сервис
Заметил, что веб-серер стал постоянно падать. Работать с кошельком было невозможно. Погуглил, понял, что надо делать сервис. На этот раз сложностей было уже не так много, исходник тут. ShortcutIcon() создает иконку на рабочем столе.
Доступ к иконке
При появлении уведомлений нужно указать иконку. Пришлось разбираться как в android устроена работа с ресурсами. Примерно понял, что нужно сгенерировать R.jar и подключить его при генерации dex-файла.
Погуглил, как генерить java файлы, получилась такая команда:
aapt package -v -f -J /home/z/go-projects/src/github.com/c-darwin/dcoin-go/ -S /home/z/go-projects/src/github.com/c-darwin/dcoin-go/res/ -M /home/z/go-projects/src/github.com/c-darwin/dcoin-go/AndroidManifest.xml -I /home/z/android-sdk-linux/platforms/android-22/android.jar
Полученный R.java помещаем в R/org/golang/app/
mv R.java /home/z/go-projects/src/github.com/c-darwin/dcoin-go/R/org/golang/app/
И генерируем R.jar
cd R && jar cfv /home/z/go-projects/src/github.com/c-darwin/dcoin-go/R.jar
Генерим неподписанный apk
aapt package -v -f -J /home/z/go-projects/src/github.com/c-darwin/dcoin-go/ -S /home/z/go-projects/src/github.com/c-darwin/dcoin-go/res/ -M /home/z/go-projects/src/github.com/c-darwin/dcoin-go/AndroidManifest.xml -I /home/z/android-sdk-linux/platforms/android-22/android.jar -F unsigned.apk
Вытаскиваем в корень resources.arsc
unzip unsigned.apk -d apk && mv apk/resources.arsc .
Дальше нужно подправить gendex.go из gomobile:
cmd := exec.Command(
"javac",
"-source", "1.7",
"-target", "1.7",
"-bootclasspath", platform+"/android.jar",
"-classpath", "/home/z/go-projects/src/github.com/c-darwin/dcoin-go/R.jar:"+androidHome+"/extras/android/m2repository/com/android/support/support-v4/22.2.1/support-v4-22.2.1-sources.jar",
"-d", tmpdir+"/work",
)
Генерируем новый Dex
ANDROID_HOME=/home/z/android-sdk-linux go generate github.com/c-darwin/mobile/cmd/gomobile/
Затем бинарник самого gomobile
go install github.com/c-darwin/mobile/cmd/gomobile/
И наконец получаем наш apk
CGO_ENABLED=1 GOOS=android ANDROID_HOME=/home/z/android-sdk-linux gomobile build -v github.com/c-darwin/dcoin-go
В итоге, у меня получился вот такой bash скрипт для генерации apk:
./bindata.sh
echo "######## generate R.java ########"
aapt package -v -f -J /home/z/go-projects/src/github.com/c-darwin/dcoin-go/ -S /home/z/go-projects/src/github.com/c-darwin/dcoin-go/res/ -M /home/z/go-projects/src/github.com/c-darwin/dcoin-go/AndroidManifest.xml -I /home/z/android-sdk-linux/platforms/android-22/android.jar
mv R.java /home/z/go-projects/src/github.com/c-darwin/dcoin-go/R/org/golang/app/
echo "######## generate R.jar ########"
cd R
jar cfv /home/z/go-projects/src/github.com/c-darwin/dcoin-go/R.jar .
cd ../
echo "######## generate unsigned.apk ########"
aapt package -v -f -J /home/z/go-projects/src/github.com/c-darwin/dcoin-go/ -S /home/z/go-projects/src/github.com/c-darwin/dcoin-go/res/ -M /home/z/go-projects/src/github.com/c-darwin/dcoin-go/AndroidManifest.xml -I /home/z/android-sdk-linux/platforms/android-22/android.jar -F unsigned.apk
echo "######## extract resources.arsc ########"
unzip unsigned.apk -d apk
mv apk/resources.arsc .
rm -rf apk unsigned.apk
ANDROID_HOME=/home/z/android-sdk-linux go generate github.com/c-darwin/mobile/cmd/gomobile/
go install github.com/c-darwin/mobile/cmd/gomobile/
CGO_ENABLED=1 GOOS=android ANDROID_HOME=/home/z/android-sdk-linux gomobile build -v github.com/c-darwin/dcoin-go
Заключение
В следующей, заключительной статье я расскажу про gomobile и IOS. И, наконец, будет финал про Катю.
P.S.
Ищу Go-программеров в команду на хорошую з.п.
Автор: c-darwin