Разработка нативных GUI-приложений для Android-смартфонов без использования Android



Многие разработчики знают, что экосистема Android в основном ориентирована на Java и ART, что ограничивает создание полностью нативных приложений с графическим интерфейсом. Однако существует метод, позволяющий обойти эти ограничения и запускать программы, написанные на C, напрямую на железе смартфона, минуя Android. Этот подход открывает доступ к ключевым аппаратным возможностям: ускорению 3D-графики через OpenGL ES, микшеру звука, обработке ввода с различных устройств, работе с OTG, Wi-Fi и, при должных усилиях, даже с 3G-модемом. Это особенно актуально для старых устройств, чьи аппаратные характеристики часто превосходят современные бюджетные одноплатные компьютеры. В этой статье я подробно расскажу, как написать и запустить нативное приложение на C на Android-смартфоне No-Name, практически не модифицируя систему.

❯ Предпосылки и возможности


Даже устаревшие флагманские устройства обладают значительным вычислительным потенциалом, который зачастую не раскрывается из-за накладных расходов Android. Такие гаджеты могли бы стать отличной платформой для эмуляторов, игровых консолей, настольных часов или других специализированных задач. Однако необходимость изучения Android API часто отпугивает разработчиков, желающих работать ближе к железу.


Ключ к решению этой задачи лежит в понимании процесса загрузки Android-устройств:

  1. Первичный загрузчик (BootROM) инициализирует оборудование и загружает вторичный загрузчик (например, U-boot или Little Kernel).

  2. Вторичный загрузчик, основываясь на аргументах или действиях пользователя, выбирает раздел для загрузки ядра Linux.

  3. После загрузки ядра и монтирования RAM-диска запускаются системные процессы.


Именно третий этап содержит возможность для маневра. В смартфоне обычно есть несколько загрузочных разделов. Основной (boot.img) отвечает за запуск Android. Второй — раздел восстановления (recovery), предназначенный для сброса настроек или прошивки. Интересно, что рекавери загружается гораздо быстрее Android, потому что по сути является простой нативной программой на C со своим минимальным фреймворком для графики и ввода. В процессе загрузки этого режима запускается исполняемый файл в /sbin/, который и отображает знакомое меню. Почему бы не использовать этот раздел для запуска собственных программ, получив при этом доступ к большей части аппаратных функций смартфона (за исключением, возможно, модема)?


Для наглядности можно взглянуть на исходники рекавери, например, версии 2.3 (Gingerbread), где видна её относительно простая архитектура.


Используя эту информацию, мы можем написать своё приложение для Android-смартфона, полностью обойдя Android.

❯ Подготовка инструментов и окружения


Стоит отметить, что для "голого" смартфона можно писать не только на C/C++. Доступны, например, Free Pascal (FPC) или встраиваемые интерпретаторы вроде Lua или MicroPython. Однако при работе с нативным кодом важно помнить о двух ключевых моментах.


Во-первых, Android использует собственную стандартную библиотеку Bionic libc, которая несовместима с привычной glibc из настольных дистрибутивов Linux. Поэтому скомпилированную для Raspberry Pi программу просто так запустить не получится.

Во-вторых, начиная с Android 4.1, все нативные исполняемые файлы должны компилироваться с флагом -fPIE (Position Independent Executable), чтобы код не зависел от адреса загрузки в виртуальной памяти. Для версий Android до 4.0 этот флаг, наоборот, нужно убирать, так как они не поддерживают такой формат.

Для разработки понадобится Android NDK (Native Development Kit), содержащий необходимые заголовочные файлы и кросс-компиляторы. Автор использует версию r9c, отмечая, что в более новых версиях Google иногда ломает обратную совместимость. Поскольку стандартный ndk-build может не подойти, придётся писать Makefile вручную. Ниже приведён рабочий пример, где нужно лишь указать путь к NDK (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, порты которого существуют для многих устройств. Для No-Name смартфонов портирование может потребовать дополнительных усилий, но в сети есть соответствующие руководства.


Если TWRP для вашего устройства найдён, нужно извлечь образ recovery.img с помощью специальных утилит (например, MTKImgTools для чипов MediaTek). Затем в распакованном образе следует отредактировать файл init.recovery.service.rc, удалив или закомментировав строку, запускающую стандартную службу восстановления (можно просто оставить файл пустым).


Далее образ перепаковывается и прошивается на устройство с помощью соответствующего прошивальщика (для MediaTek это SP Flash Tool).


После перезагрузки в режим восстановления вы увидите застывшую заставку, но компьютер должен определить устройство. Если драйверы установлены, можно подключиться через ADB и получить доступ к терминалу.

Обратите внимание: 12 лучших смартфонов Xiaomi.

Теперь можно загрузить скомпилированную программу прямо в корень RAM-диска (помните, что он находится в оперативной памяти, и её переполнение приведёт к перезагрузке). Выполняем команды:

adb push cmdprog /: adb shell chmod 777 cmdprog ./cmdprog


Если всё сделано правильно, программа запустится и выведет результат своей работы.


Это отличный старт. Но мы обещали графику и обработку ввода, так что перейдём к практической реализации.

❯ Работа с графикой: кадровый буфер


Для рендеринга графики без оконной системы будем использовать API кадрового буфера (framebuffer) Linux, который предоставляет прямой доступ к массиву пикселей на экране. Это полностью программный метод, и его производительность напрямую зависит от разрешения экрана. На устройстве автора используется матрица IPS с разрешением 960x540 и 32-битным цветом, что является хорошим компромиссом.

Кадровый буфер поддерживает различные форматы пикселей (например, 16-битный RGB565 или 32-битный). Для работы нужно открыть устройство /dev/graphics/fb0, получить его параметры (разрешение, формат), создать отображение (mmap) буфера в память процесса и выделить дополнительный буфер для двойной буферизации, чтобы избежать мерцания.

недействительным 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);

}


Важно: если запустить нашу программу параллельно со стандартным рекавери, обе будут пытаться писать в один буфер, что приведёт к конфликту.


Далее пишем функции для отрисовки изображений, включая поддержку альфа-смешения. Для максимальной производительности в критичных к скорости функциях (вроде pixelAt) лучше избегать проверок границ внутри циклов — лишние пиксели стоит "отсекать" на этапе расчёта размеров.

__inline void pixelAt (целое число x, целое число y, байт r, байт g, байт b, плавающая альфа)

{

if(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), поэтому в функции pixelAt нужно поменять местами каналы B и R. После этого можно наслаждаться изображением на экране смартфона.


Производительность программного рендеринга невысока, но её можно улучшить оптимизацией: копированием непрозрачных областей целыми строками (сканлайнами) и удалением проверок из внутренних циклов. Для подобных целей Google даже создал простой софтверный рендерер — libpixelflinger.

Существует и альтернативный путь для динамичной графики — использование OpenGL ES (GLES), который также доступен из рекавери. Однако для этого, скорее всего, потребуется создать поверхность (Surface) для рендеринга через EGL, корректно настроив формат пикселей, а затем скопировать результат в кадровый буфер с помощью memcpy.

❯ Обработка сенсорного ввода


Графический интерфейс невозможен без обработки нажатий на экран. К счастью, механизм событий ввода в Linux универсален и прост: нужно открыть соответствующее устройство и читать из него данные в фиксированную структуру.

Устройства ввода находятся в директории /dev/input/ и имеют имена вида eventX. Чтобы определить нужное, можно загрузиться в Android и выполнить команду getevent, которая покажет все доступные устройства. В случае автора это было event2 (драйвер тачскрина MediaTek, mtk-tpd).


Чтение можно организовать в неблокирующем режиме, что удобно для интерактивных приложений. Также через эти устройства можно инжектить события, что полезно для отладки.

// Открытие устройства ввода evDev = open(INPUT_EVENT_TPD, O_RDWR | O_NONBLOCK);


После открытия читаем события с помощью read и обрабатываем их. На устройствах с резистивным экраном передаются абсолютные координаты (ABS_POSITION_X/Y), а на устройствах с мультитачем используется протокол MT (Multi-Touch). Событие нажатия — это BTN_TOUCH со значением 1, отпускания — 0. Координаты могут быть абсолютными относительно размеров экрана. В примере ниже реализована поддержка одного касания, но её можно расширить для отслеживания нескольких.

недействительным 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. Этот метод будет работать практически на любом устройстве, включая китайские No-Name смартфоны с закрытыми исходниками ядра.

❯ Звук, сеть и другие компоненты


Для работы со звуком, скорее всего, потребуется использовать ALSA (Advanced Linux Sound Architecture), которая является стандартом для большинства Linux-устройств. В системе могут присутствовать устройства-эмуляторы старого протокола OSS (например, /dev/snd/dsp), но для полноценной работы понадобится библиотека alsa-lib.

Сеть и модем представляют отдельную сложность. Wi-Fi теоретически можно поднять, используя wpa_supplicant из раздела /system. А вот с модемом всё гораздо сложнее — для связи с ним требуется специальный протокол (RIL — Radio Interface Layer). Для реализации этой функциональности придётся глубоко изучить исходники ядра и драйверов конкретного чипсета. Производители вроде MediaTek часто открывают исходный код стека связи, что может стать отправной точкой.

❯ Итоги и перспективы


Как мы видим, старые Android-устройства могут обрести вторую жизнь в качестве платформы для нативных приложений, полностью минуя Android. Даже там, где нет портов полноценных десктопных дистрибутивов Linux, можно создавать полезные программы, напрямую взаимодействуя с железом.

Изучение исходных кодов прошивок и ядра от вендоров — это не только увлекательный процесс, но и ключ к глубокому пониманию работы устройства. Именно такое погружение в системные механизмы и позволило создать эту статью.

[mine]ГаджетыСмартфонLinuxPhoneITХакерыВзломПрограммированиеВстроенныеC++Настольный компьютерNixUnixKernelKernelAndroidLongpost 65 Support Emotions

Больше интересных статей здесь: Гаджеты.

Источник статьи: Исходники закрыты, но мы не сдадимся: Пишем полностью нативное GUI-приложение под No-Name смартфон без Android.