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



Для многих разработчиков приложений далеко не секрет, что экосистема Android не предполагает написания полностью нативных приложений: на этой платформе многое завязано на Java и без ART можно запускать только простые сервисы без интерфейса. Однако есть один способ написать почти только Linux без перекомпиляции ядра и при этом использовать самые интересные возможности устройства без накладных расходов в виде тяжелого Android: ускорение 3D-графики (OpenGLES), микшер звука, ввод с различных устройства, OTG, Wi-Fi и если очень постараться - даже 3G. Это открывает много разных интересных применений для старых устройств: аппаратное обеспечение смартфона часто намного мощнее, чем современные недорогие одноплатные устройства. Сегодня я покажу вам, как написать и запустить приложение, полностью написанное на C без Android, на Android-смартфоне No-Name практически без изменений. Интересный? Жду вас в статье!

❯ Что нам нужно знать?


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


Но конечно есть один способ писать нативные программы, используя при этом все ресурсы смартфона/планшета. Для этого требуется понимание того, как работает процесс загрузки на многих Android-гаджетах:

  1. Первичный загрузчик (BootROM) инициализирует часть периферийного оборудования и загружает вторичный загрузчик (U-boot/LK).

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

  3. После загрузки ядра 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, все еще можно писать нативные программы и пытаться быть полезным.

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

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

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

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