Для многих разработчиков приложений далеко не секрет, что экосистема Android не предполагает написания полностью нативных приложений: на этой платформе многое завязано на Java и без ART можно запускать только простые сервисы без интерфейса. Однако есть один способ написать почти только Linux без перекомпиляции ядра и при этом использовать самые интересные возможности устройства без накладных расходов в виде тяжелого Android: ускорение 3D-графики (OpenGLES), микшер звука, ввод с различных устройства, OTG, Wi-Fi и если очень постараться - даже 3G. Это открывает много разных интересных применений для старых устройств: аппаратное обеспечение смартфона часто намного мощнее, чем современные недорогие одноплатные устройства. Сегодня я покажу вам, как написать и запустить приложение, полностью написанное на C без Android, на Android-смартфоне No-Name практически без изменений. Интересный? Жду вас в статье!
❯ Что нам нужно знать?
Даже относительно старые устройства из флагманского сегмента обладают очень хорошими характеристиками. Часто они намного мощнее современных дешевых одиночных плат и могут выполнять самые разные задачи: эмуляция консолей, работа в качестве геймеров и даже просто изготовление настольных часов своими руками было бы здорово. Но есть одно но - это Андроид. Платформа Google может замедлять работу даже достаточно мощного оборудования, резко ограничивая возможности использования таких гаджетов. И многие программисты не очень хотят заморачиваться и изучать Android API для реализации некоторых своих проектов.
Но конечно есть один способ писать нативные программы, используя при этом все ресурсы смартфона/планшета. Для этого требуется понимание того, как работает процесс загрузки на многих Android-гаджетах:
-
Первичный загрузчик (BootROM) инициализирует часть периферийного оборудования и загружает вторичный загрузчик (U-boot/LK).
-
Вторичный загрузчик с помощью определенных аргументов (например, при нажатии кнопки) выбирает, с какого раздела загрузить ядро системы.
-
После загрузки ядра Linux и подключения к рамдиску начинается выполнение системных процессов.
Только в третьем абзаце лежит ключ к методу, который мы будем использовать. Дело в том, что в смартфоне обычно несколько загрузочных разделов и на каждом свой образ ядра Linux со своим рамдиском. Первый — всем известный моддерам boot.img, отвечающий за загрузку системы и инициализацию железа/монтирование разделов/подготовку среды к работе (файлы .rc) и запуск основного процесса Android — zygote. Здесь используется собственная реализация инициализации Android.
Вторая часть, не менее знакомая многим, это рекавери, которая отвечает за так называемый режим восстановления, где мы можем сбросить данные до заводских настроек/удалить кеши или прошить кастомную прошивку. Многие из вас наверняка замечали, как быстро ваше устройство загружает этот режим, намного быстрее, чем загружается обычный Android. И именно в реализации мы должны увидеть (я сознательно выбрал ветку версии 2.3 — т.е пряник для простоты):
А рекавери оказывается самой обычной нативной программой, написанной на C со своим небольшим фреймворком для работы с графикой и вводом. В процессе загрузки режима восстановления скрипт запускает одноименную программу в /sbin/, благодаря чему мы видим простое и понятное меню. Так почему бы не использовать этот раздел в своих целях и не написать свою программу самостоятельно?
Как я уже говорил выше, в этом режиме доступны многие аппаратные функции вашего смартфона, за исключением модема. Используя полученную информацию, предлагаю написать наше маленькое приложение для Android-смартфона без самого Android!
❯ Подготавливаем окружение
В первую очередь хочу отметить, что программы для «голого» смартфона нельзя писать только на C/C++. У нас есть доступ как минимум к FPC, который долгое время умел компилировать только бинарники Android. Кроме того, мы можем портировать небольшие встроенные интерпретаторы для таких языков, как lua, micropython и duktape (JS).
Но когда дело доходит до нативных программ, нужно понимать два важных правила. Во-первых, Android использует собственную реализацию стандартной библиотеки libc, bionic, а настольные дистрибутивы используют glibc. Они несовместимы друг с другом — вот почему вы не можете просто взять и запустить консольную программу, например, для Raspberry Pi.
И второе правило — начиная с версии 4.1 Android требует, чтобы все нативные программы компилировались в режиме -fPIE — то есть код вывода не должен зависеть от адреса загруженной в виртуальную память программы. Для этого достаточно добавить ключ -fPIE, но учтите, что если вы разрабатываете приложение для Android 4.0 и старше, fPIE нужно, наоборот, убрать — старые версии Android не поддерживают такой способ генерации кода и будут сбой с ошибкой сегментации.
Для разработки нам понадобится ndk — в нем есть все необходимые заголовки и компиляторы для нашей работы. Я использую ndk r9c, потому что он может регулярно ломать что-то в последних версиях Google.
к сожалению, ndk-build здесь не сработает, поэтому Makefile придется писать самостоятельно. Я скомпилировал полностью рабочий Makefile, который без проблем скомпилирует действующую программу, все, что вам нужно сделать, это изменить NDK_DIR.
NDK_DIR=D:/android-ndk-r11c/
TOOLCHAIN_DIR = $(NDK_DIR)toolchains/arm-linux-androideabi-4.9/готовый/windows-x86_64/bin/
Gcc=$(TOOLCHAIN_DIR)arm-linux-androideabi-g++
PLAT_DIR = $(NDK_DIR)платформы/android-17/arch-arm/usr/
LINK_LIBS = -l:libEGL.so -l:libGLESv1_CM.so
OUTPUT_NAME = cmdprog
строить:
$(GCC) -I $(PLAT_DIR)include/ -L $(PLAT_DIR)lib/ -fPIE -Wl,-dynamic-linker=/sbin/linker $(LINK_LIBS) -static -o $(OUTPUT_NAME) main.cpp micro2d .cpp
После этого напишем простенькую программу, которая напечатает «Тест» и скомпилирует ее.
❯ Деплоим на устройство
Несмотря на загрузку в режим восстановления, нам все равно будет доступен adb, где мы сможем запускать и отлаживать нашу программу. Это очень удобно, но по умолчанию adb включен только в TWRP, который сначала нужно найти или портировать для своего устройства (есть порты для большинства старых брендовых устройств, нужно самому портировать на ноннейм - есть гайды в Интернете). Есть ли TWRP для вашего устройства? Отлично, извлеките recovery.img с помощью так называемой "кухни" (как вариант MTKImgTools):
откройте init.recovery.service.rc и уберите оттуда запуск одноименной службы (можно просто оставить файл пустым).
Перепаковываем образ тем же MTKImgTools и прошиваем прошивальщиком для вашего устройства - в моем случае это SP Flash Tool (MediaTek):
Входим в режим восстановления и видим зависшую заставку устройства и звук подключения устройства к ПК. Если у вас установлены драйвера, вы можете легко войти в оболочку adb и войти в терминал для управления устройством.
Обратите внимание: 12 лучших смартфонов Xiaomi.
Теперь можно закинуть программу - прямо в корень рамдиска (программа пишется в оперативную память, но если она переполнится, телефон уйдет на перезагрузку - будьте с этим осторожны). Мы пишем:adb push cmdprog /: adb shell chmod 777 cmdprog ./cmdprog
И мы видим результат. Наша программа запущена!
Это просто здорово. Однако я вам обещал, что мы напишем программу, умеющую отображать графику и обрабатывать ввод, предлагаю перейти к практической реализации!
❯ Выводим графику
Для рендеринга графики без оконных систем мы будем использовать API кадрового буфера Linux, который позволяет нам напрямую обращаться к массиву пикселей на экране. Однако учтите, что этот метод полностью программный и может быть медленным для вашего приложения: скорость работы прямо пропорциональна разрешению экрана вашего устройства. Чем выше разрешение, тем ниже скорость заполнения. В моем случае матрица с разрешением 960х540, 32 млн цветов, IPS - очень хорошо, согласны?
Кадровый буфер Linux может обрабатывать широкий спектр форматов пикселей, так что имейте это в виду. На некоторых устройствах это может быть 16-битный формат (262 тысячи цветов, RGB565), но на моем он оказался 32-битным с построчной подстройкой (это тоже помните). 32-битный формат. Работать с ним просто: открываем устройство /dev/graphics/fb0, получаем параметры (разрешение, формат пикселей), создаем mmap для отображения буфера пикселей на экране в память нашего процесса и выделяем лишний буфер для double буферизация, чтобы избежать неприятного мерцания.
недействительным m2dAllocFrameBuffer()
{
fbDev = открыть (PRIMARY_FB, O_RDWR);
fb_var_screeninfo vInfo; fb_fix_screeninfo fИнформация;
ioctl(fbDev, FBIOGET_VSCREENINFO, &vInfo);
ioctl(fbDev, FBIOGET_FSCREENINFO, &fInfo); fbDesc.width = vInfo.xres;
fbDesc.height = vInfo.yres;
fbDesc.pixels = (unsigned char*)mmap(0, fInfo.smem_len, PROT_WRITE, MAP_SHARED, fbDev, 0); ф
bDesc.length = fInfo.smem_len; fbDesc.lineLength = fInfo.line_length;
backBuffer = (unsigned char*)malloc(fInfo.smem_len); набор памяти (backBuffer, 128, fInfo.smem_len);
printf("Буфер кадра %s %ix%ix%i\n", (char*)&fInfo.id, fbDesc.width, fbDesc.height, vInfo.bits_per_pixel, fInfo.type);
}
Если вы не сделаете предыдущий шаг и запустите нашу программу параллельно с восстановлением, обе будут пытаться «перекрыть» друг друга — своего рода состояние гонки:
После этого пишем простые функции для блитинга изображений (в том числе и с альфа-смешением). В инлайны и критичные к скорости функции лучше не добавлять условия для проверки лимитов нашего буфера — лучше «отрезать» лишнее на этапе расчета ширины/высоты:
__inline void pixelAt (целое число x, целое число y, байт r, байт g, байт b, плавающая альфа)
{
if(x < 0 || y < 0 || x >= fbDesc.width || y >= fbDesc.height) return;
unsigned char* absPtr = &backBuffer[(y * fbDesc.lineLength) + (x * 4)];
если (альфа >= 0,99f)
{
абсПтр[0] = б;
абсПтр[1] = г;
абсПтр[2] = г;
}
другой {
absPtr[0] = (byte)(b * alpha + absPtr[0] * (1.0f - alpha));
absPtr[1] = (byte)(g * alpha + absPtr[1] * (1.0f - alpha));
absPtr[2] = (byte)(r * альфа + absPtr[2] * (1.0f - альфа));
} абсПтр[3] = 255; }
for(int я = 0; я <изображение->высота; я++)
{
for(int j = 0; j < изображение-> ширина; j++)
{
byte* ptr = &image->pixels[((image->height - i) * image->width + j) * 3]; pixelAt(x + j, y + i, ptr[0], ptr[1], ptr[2], alpha);
}
}
изображение->
И загрузчик TGA:
CImage* m2dLoadImage(char* имя файла) {
ФАЙЛ* f = fopen(имя файла, "r");
printf("m2dLoadImage: Загрузка %s\n", имя файла);
если(!ф)
{
printf("m2dLoadImage: Не удалось загрузить %s\n", имя файла);
вернуть 0;
}
CTgaHeader hdr;
fread(&hdr, sizeof(hdr), 1, f);
если (hdr.paletteType)
{
printf("m2dLoadImage: изображения палитры не поддерживаются\n");
вернуть 0;
}
если(hdr.bpp != 24) {
printf("m2dLoadImage: BPP не поддерживается\n");
вернуть 0;
}
byte* buf = (byte*)malloc(hdr.width * hdr.height * (hdr.bpp / 8));
утверждать (баф);
fread(buf, hdr.width * hdr.height * (hdr.bpp / 8), 1, f);
двуустка (f);
CIImage* ret = (CIImage*)malloc(sizeof(CIImage));
рет->ширина = hdr.width;
ret->height = hdr.height;
рет->пиксели = буфер;
printf("m2dLoadImage: Загружено %s %ix%i\n", имя файла, ret->width, ret->height);
вернуться вправо;
}
И давайте попробуем отобразить изображение:
m2dInit();
тест = m2dLoadImage("test.tga");
test2 = m2dLoadImage("habr.tga");
пока (1)
{
m2dОчистить();
m2dDrawImage(тест, 0, 0, 1.0f);
m2dDrawImage(test2, tsX - (test2->width/2), tsY - (test2->height/2), 0.5f);
m2dFlush();
}
Не забываем порядок пикселей в TGA (BGR, вместо RGB), меняем каналы b и r в pixelAt и наслаждаемся изображением на большом и крутом IPS-экране:
Производительность рендеринга не очень высокая, но если оптимизировать код (скопировать непрозрачные изображения сразу сканлайнами и убрать чеки в инлайнах), то пойдет чуть быстрее. Google создал для таких целей свой простой софт-рендер — libpixelflinger.
Есть альтернатива быстрой и динамичной графике: использовать GLES, который тоже без проблем доступен из рекавери. Но насколько я знаю (исходники драйвера посмотреть не могу), указать фреймбуфер как окно не получится, поэтому в качестве Surface для цель рендеринга, для которой необходимо задать правильный формат пикселей (см документацию .EGL). Рисуем там картинку с аппаратным ускорением и потом копируем в фреймбуфер с помощью memcpy.
❯ Обработка нажатий
Тем не менее, мы не говорим ни о каких программах с графическим интерфейсом, если мы не можем справиться с давлением на экран с полным мультитач! К счастью, даже механизм обработки событий в Linux очень прост и приятен: мы точно так же открываем устройство и просто считываем из него события в фиксированную структуру. Это функция, которая мне очень нравится в архитектуре Linux!
Каждое устройство, которое может отправлять данные о кликах, находится в папке /dev/input/ и имеет имя, похожее на событие. Как узнать нужный нам расклад? Нам нужен mtk-tpd — реализация драйвера тачскрина от MediaTek (у вашего чипсета может быть свой), для этого загружаемся в Android и пишем getevent. Он покажет доступные устройства ввода в системе — в моем случае это event2:
Читать из события можно как в блокирующем, так и в неблокирующем режиме, нам нужен второй. Также в них можно инжектить события, как я показывал в статье про создание собственной консоли из планшета с неработающим сенсорным экраном:
// Открытие устройства ввода evDev = open(INPUT_EVENT_TPD, O_RDWR | O_NONBLOCK);
После этого читаем события с помощью read и обрабатываем их. На устройствах с резистивным сенсорным экраном просто передается ABS_POSITION_X, на устройствах с поддержкой мультитач используется протокол MT. Когда пользователь нажимает на экран, отправляется нажатие BTN_TOUCH со значением 1, а когда пользователь отпускает его, отправляется соответствующее BTN_TOUCH со значением 0. Разные драйверы сенсорных экранов используют разные системы координат (насколько я понимаю), в для MediaTek это абсолютные координаты экрана (с точностью до высоты). На данный момент я реализовал поддержку только одного нажатия, но при желании вы можете добавить отслеживание нескольких кликов:
недействительным m2dUpdateInput()
{
input_eventev;
интервал справа = 0;
while((ret = read(evDev, &ev, sizeof(input_event)) != -1))
{
if(ev.code == ABS_MT_POSITION_X) tsState.x = ev.value;
if(ev.code == ABS_MT_POSITION_Y) tsState.y = ev.value;
if(ev.code == BTN_TOUCH) tsState.isPressed = ev.value == 1;
}
tsState.cb(tsState.isPressed, tsState.x, tsState.y); }
Теперь мы можем «носить» логотип Хабра по всему экрану:
voidonTouchUpdate (bool isTouching, int x, int y) {
если (касается)
{
цХ=х;
цУ = у;
}
}
int main(int argc, char** argv) {
printf("тест\n");
m2dInit();
тест = m2dLoadImage("test.tga");
test2 = m2dLoadImage("habr.tga");
printf("Том: %i %i\n", vol, muteState);
m2dAttachTouchCallback(&onTouchUpdate);
в то время как (1) {
m2dUpdateInput();
m2dОчистить();
m2dDrawImage(тест, 0, 0, 1.0f);
m2dDrawImage(test2, tsX - (test2->width/2), tsY - (test2->height/2), 0.5f);
m2dFlush();
}
вернуть 0;
}
В общем, это уже можно назвать минимальным требованием минимум для взаимодействия с устройством и максимум использования всех функций без Android. Также этот метод будет работать практически на любом устройстве, в том числе и на китайском NoName, где не упоминаются исходники ядра. Теперь вы можете попробовать использовать свое старое Android-устройство для чего-то полезного, не изучая Android API.
❯ Звук, модем и другие возможности
Для звука нам нужно использовать ALSA — так как эта аудиоподсистема сейчас используется в большинстве устройств на Linux. Судя по всему это режим эмуляции старого и удобного OSS, так как присутствуют устройства /dev/snd/dsp. Однако вывод из любого PCM-потока в нем ничего не дает, так что ALSA-lib нам пригодится.
Другая проблема касается модема и сети. А если вайфай еще можно поднять (wpa_supplicant можно достать из раздела /system/), то с модемом будут проблемы - простого протокола для связи с ним нет, а местами придется попотеть чтобы он функционировал. Смело изучайте исходники ядра (MediaTek охотно делится реализацией всего вообще - это и RIL, и драйвер для связи с модемом) и смотрите интересующие вас фишки!
❯ Заключение
Как мы видим, старые устройства все еще могут быть полезны в какой-то области, даже без Android на борту. На тех устройствах, где нет порта Ubuntu или обычного десктопного Linux, все еще можно писать нативные программы и пытаться быть полезным.
Смело лазайте и изучайте исходники поставщиков — это дает понимание того, как работают устройства изнутри. Собственно, благодаря такому ежедневному копанию исходников системы и появилась эта статья! :)
Больше интересных статей здесь: Гаджеты.
Источник статьи: Исходники закрыты, но мы не сдадимся: Пишем полностью нативное GUI-приложение под No-Name смартфон без Android.