版本比较

密钥

  • 该行被添加。
  • 该行被删除。
  • 格式已经改变。
目录
minLevel1
maxLevel23
outlinefalse
styledefault
typelist
separatorbrackets
printabletrue

背景介绍

请先读本段!请先读本段!请先读本段!

本文是在OpenFDE上实现linux窗口和Android窗口融合的一种方案预研和设想,因为方案有些复杂,这段文字作为一个前情提要,描述一下背景知识,不然直接开始容易让人懵逼。

...

Termux 是一个 Android 终端应用程序和 Linux 环境。直观的来看,Termux 是运行在 Android 上的 terminal,不需要root,运行于内部存储(不在SD卡上),目录位于本应用的file目录,配置了相应的环境变量,包安装命令是pkg install xxx,实际是一个封装APT的脚本。

源码地址:https://github.com/termux/termux-app

...

环境变量配置:

...

安装库路径:/data/data/com.termux/files/usr/lib

...

当你使用termux作为终端来安装和运行程序时,应该是从代码仓库编译https://github.com/termux/termux-packages ,并发布到apt源的程序。

Termux-packages该项目包含用于构建 Termux Android 应用程序包的脚本和补丁。简单对比了一下补丁文件,项目构建的方式主要是将linux的编译选项改为Android,如果原本项目就支持Android编译,即可以最简单的直接编译出程序,否则就要先安装在Android上本来不存在的依赖。

Termux-X11 是一个成熟的 X 服务器。它是使用 Android NDK 构建的,并针对与 Termux 一起使用进行了优化。

...

代码块
export DISPLAY=:0
firefox
wps
...
//启动合成器、窗口管理器
xfwm4
i3

效果图:

...

Termux-x11原理浅析

从以上的执行过程来看,启动x11 server(即termux-x11)的方式是在termux终端运行termux-x11 :0命令。

...

代码块
public class CmdEntryPoint extends ICmdEntryInterface.Stub {
    public static final String ACTION_START = "com.termux.x11.CmdEntryPoint.ACTION_START";
    public static final int PORT = 7892;
    public static final byte[] MAGIC = "0xDEADBEEF".getBytes();
    private static final Handler handler;
    public static Context ctx;
     ...

笔者理解Termux-x11有两条运行线:

  • termux-->app_process-->com.termux.x11.CmdEntryPoint-->startserver

  • termux-x11-->SurfaceView-->com.termux.x11.ICmdEntryInterface.windowChanged-->xserver

两条线的最后代码都是在termux-x11,并且两条线不互相干扰,server启动后不置入SurfaceView,不显示图像,server也可以正常在后台运行。

...

简单描述这个问题:将termux、termux-x11安装启动xserver的过程,做到用户无感知,交互正常,不需要pkg安装,也不需要命令行操作。

(?笔者认为的关键是,解决环境变量,库依赖的问题)

可能的最终形态:

  1. 改成普通安卓service或者系统安卓service,运行在后台;

  2. 提供一个融合应用的入口,类似VNC的《Fusion Linux Application》,用户启动后再拉起server,列出X程序,点击X程序再启动linux端的连接。

怎么使用Compositor实现窗口融合?

合成器扩展Compositor,又叫混成器,一句话形容,是用来将不同的X程序的显示内容合成成最终图像的扩展模块。既然是窗口管理程序,使用的肯定也是X协议的窗口管理协议,ICCCM/EWMH,参见 https://tronche.com/gui/x/icccm/ 。他的主要函数是从窗口获取图像,可以用来实现各种窗口特效。

使用Compositor扩展后,xserver服务端和compositor端都有相应的回调函数拿到想要的窗口的图像。

...

Compositor源码说明

[Composite extension protocol specification]  https://cgit.freedesktop.org/xorg/proto/compositeproto/tree/compositeproto.txt  
[合成器扩展功能练习]  http://www.talisman.org/~erlkonig/misc/x11-composite-tutorial/  

...

代码块
languagejava
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <libpng16/png.h>
​
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xcomposite.h>
#include <X11/extensions/Xrender.h>
//Compile hint: gcc -shared -O3 -lX11 -fPIC -Wl,-soname,prtscn -o prtscn.so prtscn.c
​
typedef int bool;
#define true 1
#define false 0
​
const int DEBUG = 0;
​
void getScreen2(const int, const int, const int, const int, const XID,
  unsigned char *);
void write_png_for_image(XImage *image, XID xid, int width, int height,
  char *filename);
typedef int (*handler)(Display *, XErrorEvent *);
XID getWindows(Display *display, Window parent, Window window, XID xid,
  int depth);
​
int main() {
​
 Display *display = XOpenDisplay("10.31.91.87:5");
 Window root = DefaultRootWindow(display);
​
 uint nwindows;
 Window root_return, parent_return, *windows;
​
 Atom a = XInternAtom(display, "_NET_CLIENT_LIST", true);
 Atom actualType;
​
 int format;
 unsigned long numItems, bytesAfter;
 unsigned char *data = 0;
​
 int status = XGetWindowProperty(display, root, a, 0L, (~0L),
 false,
 AnyPropertyType, &actualType, &format, &numItems, &bytesAfter, &data);
​
 char* window_name_return;
​
 if (status >= Success && numItems) {
  long *array = (long*) data;
  for (long k = 0; k < numItems; k++) {
   Window window = (Window) array[k];
​
   //not finding chrome window name
   printf("window found was %d \n", window);
   if (XFetchName(display, window, &window_name_return)) {
    printf("found window name for %d : %s \n", window,
      window_name_return);
   }
​
   //XMapWindow(display, parent);
   XMapWindow(display, window);
​
   XWindowAttributes attr;
​
   Status status = XGetWindowAttributes(display, window, &attr);
   if (status == 0)
    printf("Fail to get window attributes!\n");
​
   unsigned char outdata[attr.width * attr.height * 3];
​
   getScreen2(0, 0, attr.width, attr.height, window, outdata);
​
  }
  XFree(data);
 }
​
 return 0;
}
​
void getScreen2(const int xx, const int yy, const int W, const int H,
  const XID xid, /*out*/unsigned char * data) {
​
 Display *display = XOpenDisplay("10.31.91.87:5");
 Window root = DefaultRootWindow(display);
​
 // turn on --sync to force error on correct method
 //https://www.x.org/releases/X11R7.6/doc/man/man3/XSynchronize.3.xhtml
 XSynchronize(display, True);
​
 int counter = 1;
​
 // select which xid to operate on, the winder or its parent
 //XID xwid = fparent;
 XID xwid = xid;
​
 // Requests the X server to direct the hierarchy starting at window to off-screen storage
 XCompositeRedirectWindow(display, xwid, CompositeRedirectAutomatic);
​
 XWindowAttributes attr;
 Status status = XGetWindowAttributes(display, xwid, &attr);
​
 int width = attr.width;
 int height = attr.height;
 int depth = attr.depth;
​
 Pixmap xc_pixmap = XCompositeNameWindowPixmap(display, xwid);
 if (!xc_pixmap) {
  printf("xc_pixmap not found\n");
 }
​
 //XWriteBitmapFile(display, "test1.xpm", pixmap, W, H, -1, -1);
​
 XRenderPictFormat *format = XRenderFindVisualFormat(display, attr.visual);
​
 XRenderPictureAttributes pa;
​
 pa.subwindow_mode = IncludeInferiors;
​
 Picture picture = XRenderCreatePicture(display, xwid, format,
 CPSubwindowMode, &pa);
​
 char buffer[50];
 int n;
 int file_counter = 1;
​
 n = sprintf(buffer, "/tmp/%d_test%d.xpm", xid, file_counter++);
 XWriteBitmapFile(display, buffer, xc_pixmap, W, H, -1, -1);
​
 n = sprintf(buffer, "/tmp/%d_test%d.xpm", xid, file_counter++);
 XWriteBitmapFile(display, buffer, xid, W, H, -1, -1);
​
 XImage *image = XGetImage(display, xid, 0, 0, W, H, AllPlanes, ZPixmap);
 if (!image) {
  printf("XGetImage failed\n");
 }
​
 char filename[255];
 int n2;
​
 n2 = sprintf(filename, "/tmp/png_out/%d_test%d.png", xid, file_counter++);
​
 printf("filename %s \n", filename);
 write_png_for_image(image, xid, W, H, filename);
​
 //XFree(image);
 XDestroyImage(image);
 XDestroyWindow(display, root);
 XCloseDisplay(display);
}
​
void write_png_for_image(XImage *image, XID xid, int width, int height,
  char *filename) {
​
 int code = 0;
 FILE *fp;
 png_structp png_ptr;
 png_infop png_info_ptr;
 png_bytep png_row;
​
 char buffer[50];
 int n;
​
 n = sprintf(buffer, filename, xid);
​
// Open file
 fp = fopen(buffer, "wb");
 if (fp == NULL) {
  fprintf(stderr, "Could not open file for writing\n");
  code = 1;
 }
​
// Initialize write structure
 png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
 if (png_ptr == NULL) {
  fprintf(stderr, "Could not allocate write struct\n");
  code = 1;
 }
​
// Initialize info structure
 png_info_ptr = png_create_info_struct(png_ptr);
 if (png_info_ptr == NULL) {
  fprintf(stderr, "Could not allocate info struct\n");
  code = 1;
 }
​
// Setup Exception handling
 if (setjmp(png_jmpbuf (png_ptr))) {
  fprintf(stderr, "Error during png creation\n");
  code = 1;
 }
​
 png_init_io(png_ptr, fp);
​
// Write header (8 bit colour depth)
 png_set_IHDR(png_ptr, png_info_ptr, width, height, 8, PNG_COLOR_TYPE_RGB,
 PNG_INTERLACE_NONE,
 PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
​
// Set title
 char *title = "Screenshot";
 if (title != NULL) {
  png_text title_text;
  title_text.compression = PNG_TEXT_COMPRESSION_NONE;
  title_text.key = "Title";
  title_text.text = title;
  png_set_text(png_ptr, png_info_ptr, &title_text, 1);
 }
​
 png_write_info(png_ptr, png_info_ptr);
​
// Allocate memory for one row (3 bytes per pixel - RGB)
 png_row = (png_bytep) malloc(3 * width * sizeof(png_byte));
​
 unsigned long red_mask = image->red_mask;
 unsigned long green_mask = image->green_mask;
 unsigned long blue_mask = image->blue_mask;
​
// Write image data
//int xxx, yyy;
 for (int y = 0; y < height; y++) {
  for (int x = 0; x < width; x++) {
   unsigned long pixel = XGetPixel(image, x, y);
   unsigned char blue = pixel & blue_mask;
   unsigned char green = (pixel & green_mask) >> 8;
   unsigned char red = (pixel & red_mask) >> 16;
   png_byte *ptr = &(png_row[x * 3]);
   ptr[0] = red;
   ptr[1] = green;
   ptr[2] = blue;
  }
  png_write_row(png_ptr, png_row);
 }
​
// End write
 png_write_end(png_ptr, NULL);
​
// Free
 fclose(fp);
 if (png_info_ptr != NULL)
  png_free_data(png_ptr, png_info_ptr, PNG_FREE_ALL, -1);
 if (png_ptr != NULL)
  png_destroy_write_struct(&png_ptr, (png_infopp) NULL);
 if (png_row != NULL)
  free(png_row);
​
}

窗口融合原理

FDE上要实现的窗口融合效果是Linux的每个X程序跟安卓的每个应用显示效果一致,使自己的程序独有窗口。当我们能够将每个X程序的图像从xserver的合成器中分离出来的时候,方案的前置条件就成立了。当然具体实现还要从以下几个方面着手。

1. 显示重定向

显示重定向是最容易理解的,就是把X程序对应用窗口的显示内容转到另一个单独的buffer中,FDE再把这个buffer拿去安卓中做显示,即是output的处理。

...

代码块
static void lorieTimerCallback(int fd, unused int r, void *arg) {
   logh("lorieTimerCallback");
   char dummy[8];
   read(fd, dummy, 8);
   if (renderer_should_redraw() && RegionNotEmpty(DamageRegion(pvfb->damage))) {
       int redrawn = FALSE;
       ScreenPtr pScreen = (ScreenPtr) arg;
​
       loriePixmapUnlock(pScreen->GetScreenPixmap(pScreen));
       redrawn = renderer_redraw(pvfb->env, pvfb->root.flip);
       if (loriePixmapLock(pScreen->GetScreenPixmap(pScreen)) && redrawn)
           DamageEmpty(pvfb->damage);
   } else if (pvfb->cursorMoved)
       renderer_redraw(pvfb->env, pvfb->root.flip);
​
   pvfb->cursorMoved = FALSE;
}

Action 2 : 重定向后buffer的生命周期管理或者说X程序生命周期的管理

可能要拿到以下确切数据,来构建安卓窗口:

  1. connection建立过程,window创建过程,window的详细参数;

  2. window分类,哪些需要新窗口显示,哪些在内部处理;

  3. 优化damage机制;

  4. ....

2. 事件重定向

Compositor扩展并没有处理事件重定向,意思就是说他不重定向输入事件。他会将OverlayWindow 的所有输入直接传到下层窗口,相当于什么也没做。

...

  1. 平移和缩放窗口,平移是在安卓窗口上的操作,对X程序的显示其实不产生影响,甚至可以不做处理,因为重定向之后,即使有堆叠也可以拿完全显示的X程序图像。缩放在安卓上几乎不需要太多改动,但是在xserver却要重绘,resize等操作,是一个需要把操作指令转换成xserver函数的工作。

...

Action 3:事件重定向的坐标转换,窗口ConfigreNotify

  1. 缩放时处理Randr;

  2. 坐标系转换;

  3. 快捷键捕获;

  4. 文本输入移植ibus?;

  5. ...

3. 安卓端实现

相较于Xserver的改动,安卓的实现简单,在VNC上的处理都可以直接移植,遗留有一个activity多栈未解决,也不是马上必须要解决的。

...

关于output,所有的X程序的图像buffer都有两份,虽然合成器合成后的那个图像并不会做显示,但是也占用了内存,需要评估最终效果。

Action 4 :需要在安卓端研究的工作:

  1. 复杂输入事件需要验证,比如组合事件是否能直接生效;

  2. 显示数据使用翻倍,内存有没有问题;

  3. 资源配置文件的完善(字体,字符,键盘布局文件等);

  4. DecorcaptionView跟X程序操作按钮的替换;

  5. ...

...

窗口分离实验步骤

以上是原理方面的设想,当要做工程实现的时候,最好是在现有项目做一定的验证,而且只对关键部分做验证,显然显示重定向是决定方案是否可行的关键。但是之前要解决另一个问题,Android多SurfaceView渲染的问题。

Android多SurfaceView渲染

现在大部分安卓项目在使用opengl渲染的时候已经都学会自己配置EGL,而不是直接使用GlSurfaceView,好处是可以自己管理EGL环境,可以处理所有资源的申请和释放,不再跟view的生命周期绑定,不再使用原生的render,自己组织gl指令,满足各种特制化需求。

...

调试效果如下,这里的弹窗口是选项弹窗,未选特定窗口,不如看视频

...

Action 5:探究另一种可能的实现方式:Xserver多screen

每个X程序独占一个screen,所有X程序的input,output不产生关系,唯一需要处理焦点切换。

未来需要做的事 ACTION

  1. 改造termux/termux-x11;

  2. 重定向后buffer的生命周期管理或者说X程序生命周期的管理;

  3. 事件重定向的坐标转换,窗口ConfigreNotify;

  4. 复杂输入事件需要验证,比如组合事件是否能直接生效;

  5. 探究另一种可能的实现方式:Xserver多screen。

...