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

в 8:01, , рубрики: android, android_выкрутасы, bodyawm_rпрограммирование, bodyawm_ништячки, embedded, hacking, linux, nix, timeweb_статьи, UNIX, Блог компании Timeweb Cloud, ввод, выкрутасы, графика, Железо, одноплатники, Программирование, Разработка под android, Разработка под Linux, хакинг
image

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

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

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

image

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

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

Как раз в третьем пункте и лежит ключ к способу, который будем использовать мы. Дело в том, что в смартфоне обычно есть несколько boot-разделов и у каждого свой образ ядра Linux со своим ramdisk. Первый из них — это знакомый моддерам boot.img, который отвечает за загрузку системы и инициализирует железо/монтирует разделы/подготавливает окружение к работе (.rc файлы) и запускает главный процесс Android — zygote. При этом используется собственная реализация init от Android.

image

Второй, не менее знакомый многим раздел — recovery, отвечает за так называемый режим восстановления, в котором мы можем сбросить данные до заводских настроек/очистить кэши или прошить кастомную прошивку. Вероятно, многие из вас замечали, насколько быстро ваш девайс загружает этот режим, гораздо быстрее, чем загрузка обычного Android. И именно в его реализацию нам нужно заглянуть (я намеренно выбрал бранч версии 2.3 — т.е Gingerbread для простоты):

image

А recovery оказывается самой обычной нативной программой, написанной на C со своим небольшим фреймворком для работы с графикой и вводом. В процессе загрузки режима recovery, скрипт запускает одноименную программу в /sbin/, благодаря которому мы видим простую и понятную менюшку. Так почему бы не использовать этот раздел в своих целях и не написать какую-нибудь нативную программу самому?

Как я уже говорил выше, в этом режиме доступны многие аппаратные возможности вашего смартфона, за исключением модема. Используя полученную информацию, предлагаю написать наше небольшое приложение под Android-смартфон без Android сами!

Подготавливаем окружение

В первую очередь, хотелось бы отметить, что программы под «голый» смартфон можно писать не только на C/C++. Нам доступен как минимум FPC, который довольно давно умеет компилировать голые бинарники под Android. Кроме того, мы можем портировать маленькие embedded-версии интерпретаторов таких языков, как lua, micropython и duktape (JS).

Однако в случае нативных программ, есть два важных правила, которые необходимо понимать. Во-первых, в Android используется собственную реализацию стандартной библиотеки libc — bionic, в то время как на десктопных дистрибутивах используется glibc. Между собой они не совместимы — именно поэтому вы не можете просто взять и запустить консольную программу для Raspberry Pi, например.

image

А второе правило заключается в том, что начиная с версии 4.1, Android требует, чтобы все нативные программы были скомпилированы в режиме -fPIE — т. е. выходной код должен не зависеть от адреса загрузки программы в виртуальную память. Для этого достаточно добавить ключ -fPIE, однако учтите, что если вы разрабатываете программу под Android 4.0 и ниже, то fPIE наоборот необходимо убрать — старые версии Android не поддерживают такой способ генерации кода и будут вылетать с Segmentation fault.

Для разработки нам понадобится 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/prebuilt/windows-x86_64/bin/
GCC = $(TOOLCHAIN_DIR)arm-linux-androideabi-g++
PLAT_DIR = $(NDK_DIR)platforms/android-17/arch-arm/usr/

LINK_LIBS = -l:libEGL.so -l:libGLESv1_CM.so

OUTPUT_NAME = cmdprog

build:
	$(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

После этого пишем простенькую программу, которая должна вывести «Test» и компилируем её.

Деплоим на устройство

Несмотря на то, что грузиться мы будем в режим recovery, нам всё равно будет доступен adb, через который мы сможем запускать и отлаживать нашу программу. Это очень удобно, однако по умолчанию adb включен только в TWRP, который нужно сначала найти или портировать под ваш девайс (на большинство старых брендовых устройств порты есть, на нонейм придется портировать самому — гайды есть в интернете). Под ваше устройство есть TWRP? Отлично, распаковываете recovery.img с помощью так называемой «кухни» (MTKImgTools как вариант):

image

Открываете init.recovery.service.rc и убираете оттуда запуск одноименной службы (можно просто оставить файл пустым).

image

Запаковываем образ обратно тем же MTKImgTools и прошиваем флэшером для вашего устройства — в моём случае, это SP Flash Tool (MediaTek):

image

Заходим в режим рекавери и видим зависшую заставку устройства и звук подключения устройства к ПК. Если у вас установлены драйвера, то вы сможете без проблем зайти в adb shell и попасть в терминал для управления устройством. Теперь можно закинуть программу — прямо в корень рамдиска (записывается программа в ОЗУ, но при переполнении, телефон уйдет в ребут — осторожнее с этим). Пишем:

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

И видим результат. Наша программа запускается и работает!

image

Это просто отлично. Однако я ведь обещал вам, что мы напишем программу, которая сможет выводить графику и обрабатывать ввод, предлагаю перейти к практической реализации!

Выводим графику

Для вывода графики без оконных систем, мы будем использовать API фреймбуфера Linux, которое позволяет нам получить прямой доступ к массиву пикселей на экране. Однако учтите, что этот способ полностью программный и может оказаться тормозным для вашего приложения: скорость работы прямо-пропорциональна разрешению дисплея вашего устройства. Чем выше разрешение, тем ниже филлрейт. В моём случае, матрица была с разрешением 960x540, 32млн цветов, IPS — очень недурно, согласны?

Фреймбуфер Linux может работать с самыми разными форматами пикселя, имейте это ввиду. На некоторых устройствах может быть 16-битный формат (262 тысячи цветов, RGB565), на моём же оказался 32х-битный с выравниванием по строкам (имейте это также ввиду). 32х битный формат. Работать с ним легко: открываем устройство /dev/graphics/fb0, получаем параметры (разрешение, формат пикселя), делаем mmap для отображения буфера с пикселями на экране в память нашего процесса и выделяем второй буфер для двойной буферизации дабы избежать неприятных мерцаний.

void m2dAllocFrameBuffer()
{
	fbDev = open(PRIMARY_FB, O_RDWR);
	
	fb_var_screeninfo vInfo;
	fb_fix_screeninfo fInfo;
	
	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);
	fbDesc.length = fInfo.smem_len;
	fbDesc.lineLength = fInfo.line_length;
	
	backBuffer = (unsigned char*)malloc(fInfo.smem_len);
	
	memset(backBuffer, 128, fInfo.smem_len);
	
	printf("Framebuffer is %s %ix%ix%in", (char*)&fInfo.id, fbDesc.width, fbDesc.height, vInfo.bits_per_pixel, fInfo.type);
}

Если не сделать предыдущий шаг и запускать нашу программу параллельно с recovery, то они обе будут пытаться друг друга «перекрыть» — эдакий race condition:

image

После этого пишем простенькие функции для блиттинга картинок (в том числе с альфа-блендингом). В инлайнах и критичных к скорости функциям лучше не делать условия на проверку границ нашего буфера — лучше «отрезать» ненужное еще на этапе просчета ширины/высоты:

__inline void pixelAt(int x, int y, byte r, byte g, byte b, float alpha)
{
	if(x < 0 || y < 0 || x >= fbDesc.width || y >= fbDesc.height)
		return;
	
	unsigned char* absPtr = &backBuffer[(y * fbDesc.lineLength) + (x * 4)];
		
	if(alpha >= 0.99f)
	{
		absPtr[0] = b;
		absPtr[1] = g;
		absPtr[2] = r;
	}
	else
	{
		absPtr[0] = (byte)(b * alpha + absPtr[0] * (1.0f - alpha));
		absPtr[1] = (byte)(g * alpha + absPtr[1] * (1.0f - alpha));
		absPtr[2] = (byte)(r * alpha + absPtr[2] * (1.0f - alpha));
	}
	
	absPtr[3] = 255;
}

for(int i = 0; i < image->height; i++)
	{
		for(int j = 0; j < image->width; 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* fileName)
{
	FILE* f = fopen(fileName, "r");
	
	printf("m2dLoadImage: Loading %sn", fileName);

	if(!f)
	{
		printf("m2dLoadImage: Failed to load %sn", fileName);
		return 0;
	}

	CTgaHeader hdr;
	fread(&hdr, sizeof(hdr), 1, f);

	if(hdr.paletteType)
	{
		printf("m2dLoadImage: Palette images are unsupportedn");
		return 0;
	}
	
	if(hdr.bpp != 24)
	{
		printf("m2dLoadImage: Unsupported BPPn");
		return 0;
	}

	byte* buf = (byte*)malloc(hdr.width * hdr.height * (hdr.bpp / 8));
	assert(buf);

	fread(buf, hdr.width * hdr.height * (hdr.bpp / 8), 1, f);
	fclose(f);
	CImage* ret = (CImage*)malloc(sizeof(CImage));
	ret->width = hdr.width;
	ret->height = hdr.height;
	ret->pixels = buf;

	printf("m2dLoadImage: Loaded %s %ix%in", fileName, ret->width, ret->height);
	return ret;
}

И попробуем вывести картинку:

        m2dInit();
	test = m2dLoadImage("test.tga");
	test2 = m2dLoadImage("habr.tga");
	
	while(1)
	{
		m2dClear();
		m2dDrawImage(test, 0, 0, 1.0f);
		m2dDrawImage(test2, tsX - (test2->width / 2), tsY - (test2->height / 2), 0.5f);
		m2dFlush();
	}

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

Не забываем про порядок пикселей в TGA (BGR, вместо RGB), меняем канали b и r местами в pixelAt и наслаждаемся картинкой на большом и классном IPS-дисплее:

image

image

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

Есть вариант для быстрой и динамичной графики: использовать GLES, который без проблем доступен и из recovery. Однако, насколько мне известно (в исходники драйверов посмотреть не могу), указать фреймбуфер в качестве окна не получится, поэтому в качестве Surface для рендертаргета у нас будет служить Pixmap (так называемый off-screen rendering), которому нужно задать правильный формат пикселя (см. документацию EGL). Рисуем туда картинку с аппаратным ускорением и затем просто копируем в фреймбуфер с помощью memcpy.

Обработка нажатий

Однако, ни о каких GUI-программах не идёт речь, если мы не умеет обрабатывать нажатия на экране с полноценным мультитачем! Благо, даже механизм обработки событий в Linux очень простой и приятный: мы точно также открываем устройство и просто читаем из него события в фиксированную структуру. Эта черта мне очень нравится в архитектуре Linux!

Каждое устройство, которое может передавать данные о нажатиях, находится в папке /dev/input/ и имеет имя вида event. Как узнать нужный нам event? Нам нужен mtk-tpd — реализация драйвера тачскрина от MediaTek (у вашего чипсета может быть по своему), для этого загружаемся в Android и пишем getevent. Он покажет доступные в системе устройства ввода — в моём случае, это event2:

image

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

         // Open input device
	evDev = open(INPUT_EVENT_TPD, O_RDWR | O_NONBLOCK);

После этого, читаем события с помощью read и обрабатываем их. На устройствах с резистивным тачскрином, передается просто ABS_POSITION_X, на устройствах с поддержкой нескольких касаний — используется протокол MT. Когда пользователь нажал на экран, посылается нажатие BTN_TOUCH с значением 1, а когда отпускает — соответственно BTN_TOUCH с значением 0. Разные драйверы тачскрина используют разные координатные системы (насколько я понял), в случае MediaTek — это абсолютные координаты на дисплее (вплоть до ширины и высоты). На данный момент, я реализовал поддержку только одного касания, но при желании можно добавить трекинг нескольких нажатий:

void m2dUpdateInput()
{
	input_event ev;
	int ret = 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);
}

Теперь мы можем «возить» логотип Хабра по всему экрану:


void onTouchUpdate(bool isTouching, int x, int y)
{
	if(isTouching)
	{
		tsX = x;
		tsY = y;
	}
}

int main(int argc, char** argv)
{
	printf("Testn");
	
	m2dInit();
	test = m2dLoadImage("test.tga");
	test2 = m2dLoadImage("habr.tga");
	printf("Volume: %i %in", vol, muteState);
	
	m2dAttachTouchCallback(&onTouchUpdate);
	
	while(1)
	{
		m2dUpdateInput();
		
		m2dClear();
		m2dDrawImage(test, 0, 0, 1.0f);
		m2dDrawImage(test2, tsX - (test2->width / 2), tsY - (test2->height / 2), 0.5f);
		m2dFlush();
	}
	
	return 0;
}

image

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

Звук, модем и другие возможности

Для звука нам придётся использовать ALSA — поскольку эта подсистема звука сейчас используется в большинстве устройств на Linux. Судя по всему, тут есть режим эмуляции старого и удобного OSS, поскольку устройства /dev/snd/dsp присутствует. Однако, вывод в него какого либо PCM-потока не даёт ничего, поэтому нам пригодится ALSA-lib.

Другой вопрос касается модема и сети. И если Wi-Fi ещё можно поднять (wpa_supplicant можно взять из раздела /system/), то с модемом будут проблемы — нет единого протокола по общению с ним и кое-где, чтобы его заставить работать, нужно будет немного попотеть. Не стесняйтесь изучать исходники ядра (MediaTek охотно делится реализацией вообще всего — там и RIL, и драйвер общения с модемом) и смотреть интересующие вас фишки!

Заключение

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

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


Возможно, захочется почитать и это:

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

Автор: Богдан

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js