Let Termux:X11 run without sharedUserId="com.termux" (#31)

* Let Termux:X11 run without sharedUserId="com.termux"
Make Termux:X11 start user-defined commands in Termux ($PREFIX/libexec/termux-x11/termux-startx11)
Make debug_build.yml build companion package for termux and upload it as an artifact.
Update README.md
This commit is contained in:
Twaik Yont 2021-10-03 13:33:10 +03:00 committed by GitHub
parent db575f7887
commit f9a9ce3167
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1973 additions and 133 deletions

View File

@ -17,6 +17,9 @@ jobs:
- name: Build
run: |
./gradlew assembleDebug
- name: Build companion package
run: |
./build_termux_package
- name: Store generated APK file
uses: actions/upload-artifact@v2
with:

3
.gitignore vendored
View File

@ -36,3 +36,6 @@ Thumbs.db
#files containing github tokens
github.properties
*.cxx
app/.cxx/*

View File

@ -1,25 +1,25 @@
# Termux:X11
[![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux)
A [Termux](https://termux.com) add-on app providing Android frontend for Xwayland.
When developing (or packaging), note that this app needs to be signed with the same key as the main Termux app in order to have the permission to execute scripts.
## About
Termux:X11 uses [Wayland](https://wayland.freedesktop.org/) display protocol. a modern replacement and the predecessor of the [X.org](https://www.x.org/wiki) server
Termux:X11 uses [Wayland](https://wayland.freedesktop.org/) display protocol. a modern replacement and the predecessor of the [X.org](https://www.x.org/wiki) server.
Pay attention that it is not a full-fledged Wayland server and it can not handle Wayland apps except Xwayland.
## How does it work?
the Termux:X11 app writes files through `$PREFIX/tmp` in Termux directory by default and creates wayland sockets through that directory, this takes advantage of `sharedUserId` AndroidManifest attribute
The Termux:X11 app's companion package executable creates socket through `$XDG_RUNTIME_DIR` in Termux directory by default.
the wayland sockets is the way for the graphical applications to communicate with. Termux X11 applications do not have wayland support yet, this kind of setup may not be straightforward and therefore additional packages should be installed in order for X11 applications to be run in Termux:X11
The wayland sockets is the way for the graphical applications to communicate with. Termux X11 applications do not have wayland support yet, this kind of setup may not be straightforward and therefore additional packages should be installed in order for X11 applications to be run in Termux:X11
## Setup Instructions
for this one. you must enable the `x11-repo` repository can be done by executing `pkg install x11-repo` command
For this one. you must enable the `x11-repo` repository can be done by executing `pkg install x11-repo` command
for X applications to work, you must install `Xwayland` packages. you can do that by doing
For X applications to work, you must install Termux-x11 companion package. You can do that by doing
```
pkg install xwayland
pkg install termux-x11
```
## Running Graphical Applications
@ -28,7 +28,7 @@ to work with GUI applications, start Termux:X11 first. a toast message saying `S
then you can start your desired graphical application by doing:
```
~ $ export XDG_RUNTIME_DIR=${TMPDIR}
~ $ Xwayland :1 >/dev/null &
~ $ termux-x11 :1 >/dev/null &
~ $ env DISPLAY=:1 xfce4-session
```
You may replace `xfce4-session` if you use other than Xfce
@ -42,5 +42,68 @@ You can fix this in your window manager settings (in the case of xfce4 and lxqt
![image](./img/dpi-scale.png)
## Using with 3rd party apps
It is posssible to use Termux:X11 with 3rd party apps.
You should start Termux:X11's activity with providing some additional data.
```
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import com.termux.x11.common.ITermuxX11Internal;
...
private final String TermuxX11ComponentName = "com.termux.x11/.TermuxX11StarterReceiver";
private void startTermuxX11() {
Service svc = new Service();
Bundle bundle = new Bundle();
bundle.putBinder("", svc);
Intent intent = new Intent();
intent.putExtra("com.termux.x11.starter", bundle);
ComponentName cn = ComponentName.unflattenFromString(TermuxX11ComponentName);
if (cn == null)
throw new IllegalArgumentException("Bad component name: " + TermuxX11ComponentName);
intent.setComponent(cn);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|
Intent.FLAG_ACTIVITY_SINGLE_TOP);
}
class Service extends ITermuxX11Internal.Stub {
@Override
public ParcelFileDescriptor getWaylandFD() throws RemoteException {
/*
* nativeObtainWaylandFd() should create wayland-0
* socket in your $XDG_RUNTIME_DIR and return it's
* fd. You should not "listen()" this socket.
*/
int fd = nativeObtainWaylandFd();
return ParcelFileDescriptor.adoptFd(fd);
}
@Override
public ParcelFileDescriptor getLogFD() throws RemoteException {
/*
* nativeObtainLogFd() should create file that should
* contain log. Pay attention that if you choose tty/pty
* or fifo file Android will not allow writing it.
* You can use `pipe` system call to create pipe.
* Do not forget to change it's mode with `fchmod`.
*/
int fd = nativeObtainLogFd();
return ParcelFileDescriptor.adoptFd(fd);
}
@Override
public void finish() throws RemoteException {
/*
* Termux:X11 cals this function to to notify calling
* process that init stage was completed
* successfully.
*/
}
}
```
# License
Released under the [GPLv3 license](https://www.gnu.org/licenses/gpl-3.0.html).

View File

@ -5,9 +5,10 @@ android {
defaultConfig {
applicationId "com.termux.x11"
minSdkVersion 24
//noinspection OldTargetApi
targetSdkVersion 28
versionCode 7
versionName "1.02.04"
versionCode 9
versionName "1.02.06"
}
signingConfigs {
@ -30,6 +31,11 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
externalNativeBuild {
ndkBuild {
path "src/main/jni/Android.mk"
@ -47,6 +53,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation project(path: ':common')
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

View File

@ -3,8 +3,9 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.termux.x11"
android:installLocation="internalOnly"
android:sharedUserId="com.termux">
>
<uses-permission android:name="com.termux.permission.RUN_COMMAND" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"
@ -18,16 +19,11 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning"
android:debuggable="true">
>
<service
android:name=".LorieService"
android:enabled="true"
android:exported="false"/>
<service
android:name=".LorieTestService"
android:enabled="true"
android:exported="true"
android:process="com.termux.x11.separated"/>
<activity android:name=".MainActivity"
android:theme="@style/NoActionBar"
android:launchMode="singleInstance"
@ -50,8 +46,13 @@
android:supportsPictureInPicture="false"
android:resizeableActivity="true">
</activity>
<activity android:name=".TermuxX11StarterReceiver"
android:excludeFromRecents="true"
android:exported="true" />
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
<meta-data android:name="android.allow_multiple_resumed_activities" android:value="true" />
</application>
<queries>
<package android:name="com.termux" />
</queries>
</manifest>

View File

@ -1,12 +0,0 @@
// ITermuxService.aidl
package com.termux.x11;
import android.view.Surface;
interface ITermuxService {
oneway void windowChanged(in Surface surface, int width, int height);
oneway void pointerMotion(int x, int y);
oneway void pointerScroll(int axis, float value);
oneway void pointerButton(int button, int type);
oneway void keyboardKey(int key, int type, int shift, String characters);
}

BIN
app/src/main/assets/X11.zip Normal file

Binary file not shown.

View File

@ -2,6 +2,7 @@ package com.termux.x11;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@ -10,15 +11,22 @@ import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.provider.Settings;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;
import android.util.DisplayMetrics;
import android.util.Log;
@ -30,13 +38,21 @@ import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Toast;
import android.graphics.PixelFormat;
@SuppressWarnings({"ConstantConditions", "SameParameterValue"})
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@SuppressWarnings({"ConstantConditions", "SameParameterValue", "SdCardPath"})
@SuppressLint({"ClickableViewAccessibility", "StaticFieldLeak"})
public class LorieService extends Service {
static final String LAUNCHED_BY_COMPATION = "com.termux.x11.launched_by_companion";
static final String ACTION_STOP_SERVICE = "com.termux.x11.service_stop";
static final String ACTION_START_FROM_ACTIVITY = "com.termux.x11.start_from_activity";
static final String ACTION_START_PREFERENCES_ACTIVITY = "com.termux.x11.start_preferences_activity";
@ -46,7 +62,7 @@ public class LorieService extends Service {
//private
//static
long compositor;
private static ServiceEventListener listener = new ServiceEventListener();
private static final ServiceEventListener listener = new ServiceEventListener();
private static MainActivity act;
private TouchParser mTP;
@ -70,14 +86,33 @@ public class LorieService extends Service {
}
}
@SuppressLint("BatteryLife")
@SuppressLint({"BatteryLife", "ObsoleteSdkInt"})
@Override
public void onCreate() {
if (isServiceRunningInForeground(this, LorieService.class)) return;
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
String Xdgpath = preferences.getString("CustXDG", "/data/data/com.termux/files/usr/tmp/");
compositor = createLorieThread(Xdgpath);
if (isServiceRunningInForeground(this, LorieService.class)) return;
String datadir = getApplicationInfo().dataDir;
String[] dirs = {
datadir + "/files/locale",
datadir + "/files/xkb",
};
for (String dir : dirs) {
if (!(new File(dir)).exists()) {
Log.e("LorieService", dir + " does not exist. Unpacking");
try {
InputStream zipStream = getAssets().open("X11.zip");
File targetDirectory = new File(datadir + "/files/");
unzip(zipStream, targetDirectory);
break;
} catch (IOException e) {
e.printStackTrace();
}
}
}
compositor = createLorieThread();
if (compositor == 0) {
Log.e("LorieService", "compositor thread was not created");
@ -189,6 +224,49 @@ public class LorieService extends Service {
return START_REDELIVER_INTENT;
}
private static void sendRunCommandInternal(Context ctx) {
Intent intent = new Intent();
intent.setClassName("com.termux", "com.termux.app.RunCommandService");
intent.setAction("com.termux.RUN_COMMAND");
intent.putExtra("com.termux.RUN_COMMAND_PATH",
"/data/data/com.termux/files/usr/libexec/termux-x11/termux-startx11");
intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", true);
Log.d("LorieService", "sendRunCommand: " + intent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ctx.startForegroundService(intent);
} else {
ctx.startService(intent);
}
}
public static void sendRunCommand(AppCompatActivity act) {
final String ERROR_MESSAGE =
"It is impossible to start without " +
"com.termux.permission.RUN_COMMAND permission. " +
"Sorry.";
if (act.checkSelfPermission("com.termux.permission.RUN_COMMAND") == PackageManager.PERMISSION_GRANTED) {
LorieService.sendRunCommandInternal(act);
} else {
Log.d("MainActivity", "We have no permission to sendRunCommand(). Requesting it.");
ActivityResultLauncher<String> requestPermissionLauncher =
act.registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
if (isGranted) {
sendRunCommandInternal(act);
} else {
new AlertDialog.Builder(act)
.setTitle("Insufficient permission")
.setMessage(ERROR_MESSAGE)
.setPositiveButton(android.R.string.yes,
(dialog, which) -> act.finish())
.setIcon(android.R.drawable.ic_dialog_alert)
.show();
}
});
requestPermissionLauncher.launch("com.termux.permission.RUN_COMMAND");
}
}
@Override
public void onDestroy() {
@ -373,6 +451,58 @@ public class LorieService extends Service {
}
}
static final Handler handler = new Handler();
private abstract static class Task implements Runnable {
public Task() {
handler.post(this);
}
@Override
public void run() {
LorieService svc = getInstance();
if (svc == null || svc.compositor == 0 || act == null) {
handler.postDelayed(this, 100);
return;
}
run(svc);
}
public abstract void run(LorieService svc);
}
public static void adoptWaylandFd(int fd) {
new Task() {
@Override
public void run(LorieService svc) {
svc.passWaylandFD(fd);
}
};
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public static void unzip(InputStream zipStream, File targetDirectory) throws IOException {
try (ZipInputStream zis = new ZipInputStream(zipStream)) {
ZipEntry ze;
int count;
byte[] buffer = new byte[8192];
while ((ze = zis.getNextEntry()) != null) {
File file = new File(targetDirectory, ze.getName());
File dir = ze.isDirectory() ? file : file.getParentFile();
if (!dir.isDirectory() && !dir.mkdirs())
throw new FileNotFoundException("Failed to ensure directory: " +
dir.getAbsolutePath());
if (ze.isDirectory())
continue;
try (FileOutputStream fout = new FileOutputStream(file)) {
while ((count = zis.read(buffer)) != -1)
fout.write(buffer, 0, count);
}
long time = ze.getTime();
if (time > 0)
file.setLastModified(time);
}
}
}
private void windowChanged(Surface s, int w, int h, int pw, int ph) {windowChanged(compositor, s, w, h, pw, ph);}
private native void windowChanged(long compositor, Surface surface, int width, int height, int mmWidth, int mmHeight);
@ -400,10 +530,15 @@ public class LorieService extends Service {
private void keyboardKey(int key, int type, int shift, String characters) {keyboardKey(compositor, key, type, shift, characters);}
private native void keyboardKey(long compositor, int key, int type, int shift, String characters);
private native long createLorieThread(String CustXdgpath);
private void passWaylandFD(int fd) {passWaylandFD(compositor, fd);}
private native void passWaylandFD(long compositor, int fd);
private native long createLorieThread();
private native void terminate(long compositor);
public static native void startLogcatForFd(int fd);
static {
System.loadLibrary("lorie");
}

View File

@ -1,69 +0,0 @@
package com.termux.x11;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
public class LorieTestService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
static void start(Context context) {
Intent intent = new Intent(context, LorieTestService.class);
intent.setAction("start");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
}
@Override
public void onCreate() {
Intent notificationIntent = new Intent(getApplicationContext(), MainActivity.class);
notificationIntent.putExtra("foo_bar_extra_key", "foo_bar_extra_value");
notificationIntent.setAction(Long.toString(System.currentTimeMillis()));
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
//For creating the Foreground Service
int priority = Notification.PRIORITY_HIGH;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N)
priority = NotificationManager.IMPORTANCE_HIGH;
NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
String channelId = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? getNotificationChannel(notificationManager) : "";
Notification notification = new NotificationCompat.Builder(this, channelId)
.setContentTitle("Termux:Wayland Test service")
.setSmallIcon(R.drawable.ic_x11_icon)
.setContentText("foreground service")
.setContentIntent(pendingIntent)
.setOngoing(true)
.setPriority(priority)
.setShowWhen(false)
.setColor(0xFF607D8B)
.build();
startForeground(3, notification);
}
@RequiresApi(Build.VERSION_CODES.O)
private String getNotificationChannel(NotificationManager notificationManager){
String channelId = getResources().getString(R.string.app_name);
String channelName = getResources().getString(R.string.app_name);
NotificationChannel channel = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH);
channel.setImportance(NotificationManager.IMPORTANCE_NONE);
channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
notificationManager.createNotificationChannel(channel);
return channelId;
}
}

View File

@ -1,10 +1,13 @@
package com.termux.x11;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Build;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.KeyEvent;
@ -14,7 +17,6 @@ import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import android.widget.FrameLayout;
public class MainActivity extends AppCompatActivity {
@ -53,11 +55,16 @@ public class MainActivity extends AppCompatActivity {
getWindow().
getDecorView().
setPointerIcon(PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL));
Intent i = getIntent();
if (i != null && i.getStringExtra(LorieService.LAUNCHED_BY_COMPATION) == null) {
LorieService.sendRunCommand(this);
}
}
int orientation;
@Override
public void onConfigurationChanged(Configuration newConfig) {
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.orientation != orientation && kbd != null && kbd.getVisibility() == View.VISIBLE) {
@ -85,7 +92,6 @@ public class MainActivity extends AppCompatActivity {
super.onWindowFocusChanged(hasFocus);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
Window window = getWindow();
View decorView = window.getDecorView();
if (preferences.getBoolean("Reseed", true))
{

View File

@ -0,0 +1,96 @@
package com.termux.x11;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;
import android.widget.Toast;
import com.termux.x11.common.ITermuxX11Internal;
import java.io.FileOutputStream;
import java.io.IOException;
public class TermuxX11StarterReceiver extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (intent != null)
handleIntent(intent);
Intent main = new Intent(this, MainActivity.class);
main.putExtra(LorieService.LAUNCHED_BY_COMPATION, 1);
main.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(main);
finish();
}
private void log(String s) {
Log.e("NewIntent", s);
}
private void handleIntent(Intent intent) {
final String extraName = "com.termux.x11.starter";
Bundle bundle;
IBinder token;
ITermuxX11Internal svc;
ParcelFileDescriptor pfd = null;
String toastText;
// We do not use Object.equals(Object obj) for the case same intent was passed twice
if (intent == null)
return;
toastText = intent.getStringExtra("toast");
if (toastText != null)
Toast.makeText(this, toastText, Toast.LENGTH_LONG).show();
bundle = intent.getBundleExtra(extraName);
if (bundle == null) {
log("Got intent without " + extraName + " bundle");
return;
}
token = bundle.getBinder("");
if (token == null) {
log("got " + extraName + " extra but it has no Binder token");
return;
}
svc = ITermuxX11Internal.Stub.asInterface(token);
if (svc == null) {
log("Could not create " + extraName + " service proxy");
return;
}
try {
pfd = svc.getWaylandFD();
if (pfd != null)
LorieService.adoptWaylandFd(pfd.getFd());
} catch (Exception e) {
log("Failed to receive ParcelFileDescriptor");
e.printStackTrace();
}
try {
pfd = svc.getLogFD();
if (pfd != null) {
LorieService.startLogcatForFd(pfd.getFd());
}
} catch (Exception e) {
log("Failed to receive ParcelFileDescriptor");
e.printStackTrace();
}
try {
svc.finish();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}

View File

@ -1,3 +1,4 @@
ROOT_PATH := $(call my-dir)
include $(ROOT_PATH)/libxkbcommon/Android.mk
include $(ROOT_PATH)/lorie/Android.mk
include $(ROOT_PATH)/../../../../common/src/main/jni/Android.mk

View File

@ -37,11 +37,11 @@ LOCAL_SRC_FILES := \
LOCAL_CFLAGS := \
-std=c99 -Wall -Werror -Wno-unused-parameter -Wno-missing-field-initializers -Wimplicit-function-declaration \
-D_GNU_SOURCE \
-DXLOCALEDIR=\"/data/data/com.termux/files/usr/share/X11/locale\" \
-DXLOCALEDIR=\"/data/data/com.termux.x11/files/locale\" \
-DDEFAULT_XKB_LAYOUT=\"us\" \
-DDEFAULT_XKB_MODEL=\"pc105\" \
-DDEFAULT_XKB_RULES=\"evdev\" \
-DDFLT_XKB_CONFIG_ROOT=\"/data/data/com.termux/files/usr/share/X11/xkb\"
-DDFLT_XKB_CONFIG_ROOT=\"/data/data/com.termux.x11/files/xkb\"
LOCAL_C_INCLUDES := $(LOCAL_PATH)/xkbcommon $(LOCAL_PATH)/xkbcommon/src $(LOCAL_PATH)/xkbcommon/include
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/xkbcommon/
include $(BUILD_SHARED_LIBRARY)

View File

@ -11,6 +11,10 @@
#include <lorie-egl-helper.hpp>
#include <android/native_window_jni.h>
#include <xkbcommon/xkbcommon.h>
#include <sys/socket.h>
#include <dirent.h>
#define unused __attribute__((__unused__))
#define DEFAULT_WIDTH 480
#define DEFAULT_HEIGHT 800
@ -26,6 +30,7 @@ public:
void get_keymap(int *fd, int *size) override;
void window_change_callback(EGLNativeWindowType win, uint32_t width, uint32_t height, uint32_t physical_width, uint32_t physical_height);
void layout_change_callback(char *layout);
void passfd(int fd);
void on_egl_init();
void on_egl_uninit();
@ -160,6 +165,10 @@ void LorieBackendAndroid::layout_change_callback(char *layout) {
keyboard_keymap_changed();
}
void LorieBackendAndroid::passfd(int fd) {
listen(fd, 128);
wl_display_add_socket_fd(display, fd);
}
///////////////////////////////////////////////////////////
@ -180,16 +189,25 @@ static LorieBackendAndroid* fromLong(jlong v) {
}
extern "C" JNIEXPORT jlong JNICALL
JNI_DECLARE(LorieService, createLorieThread)(JNIEnv *env, jobject __unused instance, jstring CustXdgpath) {
const char *pathx = env->GetStringUTFChars(CustXdgpath, NULL);
setenv("XDG_RUNTIME_DIR", pathx, 1);
JNI_DECLARE(LorieService, createLorieThread)(unused JNIEnv *env, unused jobject instance) {
return (jlong) new LorieBackendAndroid;
}
extern "C" JNIEXPORT void JNICALL
JNI_DECLARE(LorieService, terminate)(JNIEnv __unused *env, jobject __unused instance, jlong jcompositor) {
JNI_DECLARE(LorieService, passWaylandFD)(unused JNIEnv *env, unused jobject instance, jlong jcompositor, jint fd) {
if (jcompositor == 0) return;
LorieBackendAndroid *b = fromLong(jcompositor);
char path[256] = {0};
char realpath[256] = {0};
sprintf(path, "/proc/self/fd/%d", fd);
readlink(path, realpath, sizeof(realpath));
LOGI("JNI: got fd %d (%s)", fd, realpath);
b->passfd(fd);
}
extern "C" JNIEXPORT void JNICALL
JNI_DECLARE(LorieService, terminate)(unused JNIEnv *env, unused jobject instance, jlong jcompositor) {
if (jcompositor == 0) return;
LorieBackendAndroid *b = fromLong(jcompositor);
LOGI("JNI: requested termination");
@ -198,7 +216,7 @@ JNI_DECLARE(LorieService, terminate)(JNIEnv __unused *env, jobject __unused inst
}
extern "C" JNIEXPORT void JNICALL
JNI_DECLARE(LorieService, windowChanged)(JNIEnv *env, jobject __unused instance, jlong jcompositor, jobject jsurface, jint width, jint height, jint mmWidth, jint mmHeight) {
JNI_DECLARE(LorieService, windowChanged)(JNIEnv *env, unused jobject instance, jlong jcompositor, jobject jsurface, jint width, jint height, jint mmWidth, jint mmHeight) {
if (jcompositor == 0) return;
LorieBackendAndroid *b = fromLong(jcompositor);
@ -209,7 +227,7 @@ JNI_DECLARE(LorieService, windowChanged)(JNIEnv *env, jobject __unused instance,
}
extern "C" JNIEXPORT void JNICALL
JNI_DECLARE(LorieService, touchDown)(JNIEnv __unused *env, jobject __unused instance, jlong jcompositor, jint id, jint x, jint y) {
JNI_DECLARE(LorieService, touchDown)(unused JNIEnv *env, unused jobject instance, jlong jcompositor, jint id, jint x, jint y) {
if (jcompositor == 0) return;
LorieBackendAndroid *b = fromLong(jcompositor);
LOGV("JNI: touch down");
@ -218,7 +236,7 @@ JNI_DECLARE(LorieService, touchDown)(JNIEnv __unused *env, jobject __unused inst
}
extern "C" JNIEXPORT void JNICALL
JNI_DECLARE(LorieService, touchMotion)(JNIEnv __unused *env, jobject __unused instance, jlong jcompositor, jint id, jint x, jint y) {
JNI_DECLARE(LorieService, touchMotion)(unused JNIEnv *env, unused jobject instance, jlong jcompositor, jint id, jint x, jint y) {
if (jcompositor == 0) return;
LorieBackendAndroid *b = fromLong(jcompositor);
LOGV("JNI: touch motion");
@ -227,7 +245,7 @@ JNI_DECLARE(LorieService, touchMotion)(JNIEnv __unused *env, jobject __unused in
}
extern "C" JNIEXPORT void JNICALL
JNI_DECLARE(LorieService, touchUp)(JNIEnv __unused *env, jobject __unused instance, jlong jcompositor, jint id) {
JNI_DECLARE(LorieService, touchUp)(unused JNIEnv *env, unused jobject instance, jlong jcompositor, jint id) {
if (jcompositor == 0) return;
LorieBackendAndroid *b = fromLong(jcompositor);
LOGV("JNI: touch up");
@ -236,7 +254,7 @@ JNI_DECLARE(LorieService, touchUp)(JNIEnv __unused *env, jobject __unused instan
}
extern "C" JNIEXPORT void JNICALL
JNI_DECLARE(LorieService, touchFrame)(JNIEnv __unused *env, jobject __unused instance, jlong jcompositor) {
JNI_DECLARE(LorieService, touchFrame)(unused JNIEnv *env, unused jobject instance, jlong jcompositor) {
if (jcompositor == 0) return;
LorieBackendAndroid *b = fromLong(jcompositor);
LOGV("JNI: touch frame");
@ -245,7 +263,7 @@ JNI_DECLARE(LorieService, touchFrame)(JNIEnv __unused *env, jobject __unused ins
}
extern "C" JNIEXPORT void JNICALL
JNI_DECLARE(LorieService, pointerMotion)(JNIEnv __unused *env, jobject __unused instance, jlong jcompositor, jint x, jint y) {
JNI_DECLARE(LorieService, pointerMotion)(unused JNIEnv *env, unused jobject instance, jlong jcompositor, jint x, jint y) {
if (jcompositor == 0) return;
LorieBackendAndroid *b = fromLong(jcompositor);
@ -254,7 +272,7 @@ JNI_DECLARE(LorieService, pointerMotion)(JNIEnv __unused *env, jobject __unused
}
extern "C" JNIEXPORT void JNICALL
JNI_DECLARE(LorieService, pointerScroll)(JNIEnv __unused *env, jobject __unused instance, jlong jcompositor, jint axis, jfloat value) {
JNI_DECLARE(LorieService, pointerScroll)(unused JNIEnv *env, unused jobject instance, jlong jcompositor, jint axis, jfloat value) {
if (jcompositor == 0) return;
LorieBackendAndroid *b = fromLong(jcompositor);
@ -263,7 +281,7 @@ JNI_DECLARE(LorieService, pointerScroll)(JNIEnv __unused *env, jobject __unused
}
extern "C" JNIEXPORT void JNICALL
JNI_DECLARE(LorieService, pointerButton)(JNIEnv __unused *env, jobject __unused instance, jlong jcompositor, jint button, jint type) {
JNI_DECLARE(LorieService, pointerButton)(unused JNIEnv *env, unused jobject instance, jlong jcompositor, jint button, jint type) {
if (jcompositor == 0) return;
LorieBackendAndroid *b = fromLong(jcompositor);
@ -275,7 +293,7 @@ extern "C" void get_character_data(char** layout, int *shift, int *ec, char *ch)
extern "C" void android_keycode_get_eventcode(int kc, int *ec, int *shift);
extern "C" JNIEXPORT void JNICALL
JNI_DECLARE(LorieService, keyboardKey)(JNIEnv *env, jobject __unused instance,
JNI_DECLARE(LorieService, keyboardKey)(JNIEnv *env, unused jobject instance,
jlong jcompositor, jint type,
jint key_code, jint jshift,
jstring characters_) {
@ -325,3 +343,66 @@ JNI_DECLARE(LorieService, keyboardKey)(JNIEnv *env, jobject __unused instance,
if (characters_ != NULL) env->ReleaseStringUTFChars(characters_, characters);
}
static bool sameUid(int pid) {
char path[32] = {0};
struct stat s = {0};
sprintf(path, "/proc/%d", pid);
stat(path, &s);
return s.st_uid == getuid();
}
static void killAllLogcats() {
DIR* proc;
struct dirent* dir_elem;
char path[64] = {0}, link[64] = {0};
pid_t pid, self = getpid();
if ((proc = opendir("/proc")) == NULL) {
LOGE("opendir: %s", strerror(errno));
return;
}
while((dir_elem = readdir(proc)) != NULL) {
if (!(pid = (pid_t) atoi (dir_elem->d_name)) || pid == self || !sameUid(pid))
continue;
memset(path, 0, sizeof(path));
memset(link, 0, sizeof(link));
sprintf(path, "/proc/%d/exe", pid);
if (readlink(path, link, sizeof(link)) < 0) {
LOGE("readlink %s: %s", path, strerror(errno));
continue;
}
if (strstr(link, "/logcat") != NULL) {
if (kill(pid, SIGKILL) < 0) {
LOGE("kill %d (%s): %s", pid, link, strerror);
}
}
}
}
void fork(std::function<void()> f) {
switch(fork()) {
case -1: LOGE("fork: %s", strerror(errno)); return;
case 0: f(); return;
default: return;
}
}
extern "C" JNIEXPORT void JNICALL
Java_com_termux_x11_LorieService_startLogcatForFd(unused JNIEnv *env, unused jobject thiz, jint fd) {
killAllLogcats();
LOGI("Starting logcat with output to given fd");
fork([]() {
execl("/system/bin/logcat", "logcat", "-c", NULL);
LOGE("exec logcat: %s", strerror(errno));
});
fork([fd]() {
dup2(fd, 1);
dup2(fd, 2);
execl("/system/bin/logcat", "logcat", NULL);
LOGE("exec logcat: %s", strerror(errno));
});
}

61
build_termux_package Executable file
View File

@ -0,0 +1,61 @@
#!/bin/bash
set -e
cd "$(dirname "$0")"
PACKAGE_PATH=app/build/outputs/apk/debug/termux-x11.deb
INTERMEDIATES=starter/build/intermediates
NDKBUILD_DIR=starter/build/intermediates/ndkBuild/debug/obj/local
DATA_DIR=$INTERMEDIATES/data
CONTROL_DIR=$INTERMEDIATES/control
PACKAGE_DIR=$INTERMEDIATES/package
PREFIX=$DATA_DIR/data/data/com.termux/files/usr
rm -rf $PACKAGE_PATH $DATA_DIR $CONTROL_DIR $PACKAGE_DIR
mkdir -p $PREFIX/bin/
mkdir -p $PREFIX/libexec/termux-x11
cp termux-x11 $PREFIX/bin/
cp termux-startx11 $PREFIX/libexec/termux-x11
cp starter/build/outputs/apk/debug/starter-debug.apk \
$PREFIX/libexec/termux-x11/starter.apk
for arch in armeabi-v7a arm64-v8a x86 x86_64; do
mkdir -p $PREFIX/libexec/termux-x11/$arch/
cp $NDKBUILD_DIR/$arch/libstarter.so \
$PREFIX/libexec/termux-x11/$arch/
done
mkdir -p $CONTROL_DIR
cat <<EOF > $CONTROL_DIR/control
Package: termux-x11
Architecture: all
Maintainer: Twaik Yont @twaik
Version: 1.02.06
Homepage: https://github.com/termux/termux-x11
Depends: xwayland
Description: Companion package for termux-x11 app
EOF
cat <<EOF > $CONTROL_DIR/postinst
#!/data/data/com.termux/files/usr/bin/bash
[ -z "\$PREFIX" ] && PREFIX=/data/data/com.termux/files/usr
ABI=
case \`uname -m\` in
arm) ABI=armeabi-v7a;;
aarch64) ABI=arm64-v8a;;
i686) ABI=x86;;
x86_64) ABI=x86_64;;
esac
mv \$PREFIX/libexec/termux-x11/\$ABI/libstarter.so \$PREFIX/libexec/termux-x11/
EOF
mkdir -p $PACKAGE_DIR
echo 2.0 > $PACKAGE_DIR/debian-binary
tar -cJf $PACKAGE_DIR/data.tar.xz -C $DATA_DIR .
tar -czf $PACKAGE_DIR/control.tar.gz -C $CONTROL_DIR .
ar -rsc $PACKAGE_PATH \
$PACKAGE_DIR/debian-binary \
$PACKAGE_DIR/control.tar.gz \
$PACKAGE_DIR/data.tar.xz

1
common/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

38
common/build.gradle Normal file
View File

@ -0,0 +1,38 @@
plugins {
id 'com.android.library'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.2"
defaultConfig {
minSdkVersion 21
targetSdkVersion 27
versionCode 1
versionName "1.0"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

View File

21
common/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.termux.x11.common">
</manifest>

View File

@ -0,0 +1,8 @@
package com.termux.x11.common;
// This interface is used by utility on termux side.
interface ITermuxX11Internal {
ParcelFileDescriptor getWaylandFD();
ParcelFileDescriptor getLogFD();
void finish();
}

View File

@ -0,0 +1 @@
include $(call all-subdir-makefiles)

View File

@ -1,6 +1,6 @@
#Thu Sep 09 20:28:30 IDT 2021
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=a8da5b02437a60819cad23e10fc7e9cf32bcb57029d9cb277e26eeff76ce014b
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip

View File

@ -1 +1,3 @@
include ':starter'
include ':common'
include ':app'

1
starter/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

44
starter/build.gradle Normal file
View File

@ -0,0 +1,44 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.termux.termuxam"
minSdkVersion 21
// Note: targetSdkVersion affects only tests,
// normally, even though this is packaged as apk,
// it's not loaded as apk so targetSdkVersion is ignored.
// targetSdkVersion this must be < 28 because this application accesses hidden apis
//noinspection OldTargetApi
targetSdkVersion 27
versionCode 1
versionName "0.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
externalNativeBuild {
ndkBuild {
path "src/main/jni/Android.mk"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation project(path: ':common')
//implementation fileTree(dir: 'libs', include: ['*.jar'])
//testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
}

21
starter/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.termux.x11.starter">
<application
android:label="TermuxX11Starter"
android:supportsRtl="true" />
</manifest>

View File

@ -0,0 +1,13 @@
package android.content;
import android.os.Bundle;
/**
* Stub - will be replaced by system at runtime
*/
public interface IIntentReceiver {
public static abstract class Stub implements IIntentReceiver {
public abstract void performReceive(Intent intent, int resultCode, String data, Bundle extras,
boolean ordered, boolean sticky, int sendingUser);
}
}

View File

@ -0,0 +1,197 @@
package com.termux.x11.starter;
/**
* \@hide-hidden constants
*/
public class ActivityManager {
private static final int FIRST_START_FATAL_ERROR_CODE = -100;
private static final int LAST_START_FATAL_ERROR_CODE = -1;
private static final int FIRST_START_SUCCESS_CODE = 0;
private static final int LAST_START_SUCCESS_CODE = 99;
private static final int FIRST_START_NON_FATAL_ERROR_CODE = 100;
private static final int LAST_START_NON_FATAL_ERROR_CODE = 199;
/**
* Result for IActivityManager.startVoiceActivity: active session is currently hidden.
* @hide
*/
public static final int START_VOICE_HIDDEN_SESSION = FIRST_START_FATAL_ERROR_CODE;
/**
* Result for IActivityManager.startVoiceActivity: active session does not match
* the requesting token.
* @hide
*/
public static final int START_VOICE_NOT_ACTIVE_SESSION = FIRST_START_FATAL_ERROR_CODE + 1;
/**
* Result for IActivityManager.startActivity: trying to start a background user
* activity that shouldn't be displayed for all users.
* @hide
*/
public static final int START_NOT_CURRENT_USER_ACTIVITY = FIRST_START_FATAL_ERROR_CODE + 2;
/**
* Result for IActivityManager.startActivity: trying to start an activity under voice
* control when that activity does not support the VOICE category.
* @hide
*/
public static final int START_NOT_VOICE_COMPATIBLE = FIRST_START_FATAL_ERROR_CODE + 3;
/**
* Result for IActivityManager.startActivity: an error where the
* start had to be canceled.
* @hide
*/
public static final int START_CANCELED = FIRST_START_FATAL_ERROR_CODE + 4;
/**
* Result for IActivityManager.startActivity: an error where the
* thing being started is not an activity.
* @hide
*/
public static final int START_NOT_ACTIVITY = FIRST_START_FATAL_ERROR_CODE + 5;
/**
* Result for IActivityManager.startActivity: an error where the
* caller does not have permission to start the activity.
* @hide
*/
public static final int START_PERMISSION_DENIED = FIRST_START_FATAL_ERROR_CODE + 6;
/**
* Result for IActivityManager.startActivity: an error where the
* caller has requested both to forward a result and to receive
* a result.
* @hide
*/
public static final int START_FORWARD_AND_REQUEST_CONFLICT = FIRST_START_FATAL_ERROR_CODE + 7;
/**
* Result for IActivityManager.startActivity: an error where the
* requested class is not found.
* @hide
*/
public static final int START_CLASS_NOT_FOUND = FIRST_START_FATAL_ERROR_CODE + 8;
/**
* Result for IActivityManager.startActivity: an error where the
* given Intent could not be resolved to an activity.
* @hide
*/
public static final int START_INTENT_NOT_RESOLVED = FIRST_START_FATAL_ERROR_CODE + 9;
/**
* Result for IActivityManager.startAssistantActivity: active session is currently hidden.
* @hide
*/
public static final int START_ASSISTANT_HIDDEN_SESSION = FIRST_START_FATAL_ERROR_CODE + 10;
/**
* Result for IActivityManager.startAssistantActivity: active session does not match
* the requesting token.
* @hide
*/
public static final int START_ASSISTANT_NOT_ACTIVE_SESSION = FIRST_START_FATAL_ERROR_CODE + 11;
/**
* Result for IActivityManaqer.startActivity: the activity was started
* successfully as normal.
* @hide
*/
public static final int START_SUCCESS = FIRST_START_SUCCESS_CODE;
/**
* Result for IActivityManaqer.startActivity: the caller asked that the Intent not
* be executed if it is the recipient, and that is indeed the case.
* @hide
*/
public static final int START_RETURN_INTENT_TO_CALLER = FIRST_START_SUCCESS_CODE + 1;
/**
* Result for IActivityManaqer.startActivity: activity wasn't really started, but
* a task was simply brought to the foreground.
* @hide
*/
public static final int START_TASK_TO_FRONT = FIRST_START_SUCCESS_CODE + 2;
/**
* Result for IActivityManaqer.startActivity: activity wasn't really started, but
* the given Intent was given to the existing top activity.
* @hide
*/
public static final int START_DELIVERED_TO_TOP = FIRST_START_SUCCESS_CODE + 3;
/**
* Result for IActivityManaqer.startActivity: request was canceled because
* app switches are temporarily canceled to ensure the user's last request
* (such as pressing home) is performed.
* @hide
*/
public static final int START_SWITCHES_CANCELED = FIRST_START_NON_FATAL_ERROR_CODE;
/**
* Result for IActivityManaqer.startActivity: a new activity was attempted to be started
* while in Lock Task Mode.
* @hide
*/
public static final int START_RETURN_LOCK_TASK_MODE_VIOLATION =
FIRST_START_NON_FATAL_ERROR_CODE + 1;
/**
* Result for IActivityManaqer.startActivity: a new activity start was aborted. Never returned
* externally.
* @hide
*/
public static final int START_ABORTED = FIRST_START_NON_FATAL_ERROR_CODE + 2;
/**
* Flag for IActivityManaqer.startActivity: do special start mode where
* a new activity is launched only if it is needed.
* @hide
*/
public static final int START_FLAG_ONLY_IF_NEEDED = 1<<0;
/**
* Flag for IActivityManaqer.startActivity: launch the app for
* debugging.
* @hide
*/
public static final int START_FLAG_DEBUG = 1<<1;
/**
* Flag for IActivityManaqer.startActivity: launch the app for
* allocation tracking.
* @hide
*/
public static final int START_FLAG_TRACK_ALLOCATION = 1<<2;
/**
* Flag for IActivityManaqer.startActivity: launch the app with
* native debugging support.
* @hide
*/
public static final int START_FLAG_NATIVE_DEBUGGING = 1<<3;
/**
* Result for IActivityManaqer.broadcastIntent: success!
* @hide
*/
public static final int BROADCAST_SUCCESS = 0;
/**
* Result for IActivityManaqer.broadcastIntent: attempt to broadcast
* a sticky intent without appropriate permission.
* @hide
*/
public static final int BROADCAST_STICKY_CANT_HAVE_PERMISSION = -1;
/**
* Result for IActivityManager.broadcastIntent: trying to send a broadcast
* to a stopped user. Fail.
* @hide
*/
public static final int BROADCAST_FAILED_USER_STOPPED = -2;
}

View File

@ -0,0 +1,206 @@
package com.termux.x11.starter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
/**
* Class wrapping reflection method and using named arguments for invocation
*
* Can have multiple variants of method and find one that actually exists
*/
public class CrossVersionReflectedMethod {
private final Class<?> mClass;
private Method mMethod = null;
private Object[] mDefaultArgs;
private HashMap<String, Integer> mArgNamesToIndexes;
public CrossVersionReflectedMethod(Class<?> aClass) {
mClass = aClass;
}
/**
* Try finding method method variant in reflected class
*
* @param methodName Name of method to be found
* @param typesNamesAndDefaults
* any amount of (in order, all required for each set)
* - Types (as class, used in reflection)
* - Names (used for {@link #invoke(Object, Object...)} call)
* - Default values
*/
public CrossVersionReflectedMethod tryMethodVariant(String methodName, Object... typesNamesAndDefaults) {
// If we have already found an implementation, skip next checks
if (mMethod != null) {
return this;
}
try {
// Get list of arguments for reflection call
int argCount = typesNamesAndDefaults.length / 3;
Class<?>[] refArguments = new Class<?>[argCount];
for (int i = 0; i < argCount; i++) {
Object refArgument = typesNamesAndDefaults[i * 3];
if (refArgument instanceof Class) {
refArguments[i] = (Class<?>) refArgument;
} else {
refArguments[i] = Class.forName((String) refArgument);
}
}
// Get method
mMethod = mClass.getMethod(methodName, (Class<?>[]) refArguments);
// If we're here - method exists
mArgNamesToIndexes = new HashMap<>();
mDefaultArgs = new Object[argCount];
for (int i = 0; i < argCount; i++) {
mArgNamesToIndexes.put((String) typesNamesAndDefaults[i * 3 + 1], i);
mDefaultArgs[i] = typesNamesAndDefaults[i * 3 + 2];
}
} catch (NoSuchMethodException | ClassNotFoundException ignored) {}
return this;
}
/**
* Try finding method method variant in reflected class,
* allowing method in class to have additional arguments between provided ones
*
* @param methodName Name of method to be found
* @param typesNamesAndDefaults
* any amount of (in order, all required for each set)
* - Types (as class, used in reflection)
* - Names (used for {@link #invoke(Object, Object...)} call)
* - Default values
*/
public CrossVersionReflectedMethod tryMethodVariantInexact(String methodName, Object... typesNamesAndDefaults) {
// If we have already found an implementation, skip next checks
if (mMethod != null) {
return this;
}
int expectedArgCount = typesNamesAndDefaults.length / 3;
for (Method method : mClass.getMethods()) {
if (!methodName.equals(method.getName())) {
continue;
}
// Matched name, try matching arguments
// Get list of arguments for reflection call
try {
// These are for arguments provided to this method
Class<?> expectedArgumentClass = null;
int expectedArgumentI = 0;
// This is for method arguments found through reflection
int actualArgumentI = 0;
// Parameters for method - we'll copy them to fields
// when we're sure that this is right method
HashMap<String, Integer> argNamesToIndexes = new HashMap<>();
Object[] defaultArgs = new Object[method.getParameterTypes().length];
// Iterate over actual method arguments
for (Class<?> methodParam : method.getParameterTypes()) {
// Get expected argument if we haven't it cached
if (expectedArgumentClass == null && expectedArgumentI < expectedArgCount) {
Object refArgument = typesNamesAndDefaults[expectedArgumentI * 3];
if (refArgument instanceof Class) {
expectedArgumentClass = (Class<?>) refArgument;
} else {
expectedArgumentClass = Class.forName((String) refArgument);
}
}
// Check if this argument is expected one
if (methodParam == expectedArgumentClass) {
argNamesToIndexes.put((String) typesNamesAndDefaults[expectedArgumentI * 3 + 1], actualArgumentI);
defaultArgs[actualArgumentI] = typesNamesAndDefaults[expectedArgumentI * 3 + 2];
// Note this argument is passed
expectedArgumentI++;
expectedArgumentClass = null;
} else {
defaultArgs[actualArgumentI] = getDefaultValueForPrimitiveClass(methodParam);
}
actualArgumentI++;
}
// Check if method has all requested arguments
if (expectedArgumentI != expectedArgCount) {
continue;
}
// Export result if matched
mMethod = method;
mDefaultArgs = defaultArgs;
mArgNamesToIndexes = argNamesToIndexes;
} catch (ClassNotFoundException e) {
// No such class on this system, probably okay
/*if (BuildConfig.DEBUG) {
e.printStackTrace();
}*/
}
}
return this;
}
/**
* Invoke method
*
* @param receiver Object on which we call {@link Method#invoke(Object, Object...)}
* @param namesAndValues
* Any amount of argument name (as used in {@link #tryMethodVariant(String, Object...)} and value pairs
*/
public Object invoke(Object receiver, Object ...namesAndValues) throws InvocationTargetException {
if (mMethod == null) {
throw new RuntimeException("Couldn't find method with matching signature");
}
Object[] args = mDefaultArgs.clone();
for (int i = 0; i < namesAndValues.length; i += 2) {
@SuppressWarnings("SuspiciousMethodCalls")
Integer namedArgIndex = mArgNamesToIndexes.get(namesAndValues[i]);
if (namedArgIndex != null) {
args[namedArgIndex] = namesAndValues[i + 1];
}
}
try {
return mMethod.invoke(receiver, args);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static Object getDefaultValueForPrimitiveClass(Class<?> aClass) {
if (aClass == Boolean.TYPE) {
return false;
} else if (aClass == Byte.TYPE) {
return (byte) 0;
} else if (aClass == Character.TYPE) {
return 0;
} else if (aClass == Short.TYPE) {
return (short) 0;
} else if (aClass == Integer.TYPE) {
return 0;
} else if (aClass == Long.TYPE) {
return (long) 0;
} else if (aClass == Float.TYPE) {
return 0;
} else if (aClass == Double.TYPE) {
return 0;
} else {
return null;
}
}
public boolean isFound() {
return mMethod != null;
}
}

View File

@ -0,0 +1,45 @@
package com.termux.x11.starter;
import android.os.ParcelFileDescriptor;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class ExecHelper {
public static void exec(String executable, String[] args) throws IOException {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final DataOutputStream ous = new DataOutputStream(baos);
ous.writeBytes(executable);
ous.writeByte(0);
if (args != null)
for (String s: args) {
ous.write(s.getBytes());
ous.writeByte(0); // as Null character
}
ous.flush();
ous.close();
final byte[] bArray = baos.toByteArray();
long pipe = createPipe(bArray.length);
int pipeFd = pipeWriteFd(pipe);
ParcelFileDescriptor fd = ParcelFileDescriptor.adoptFd(pipeFd);
OutputStream stream = new FileOutputStream(fd.getFileDescriptor());
int len = Math.min(bArray.length, 1024);
for (int i = 0; i < bArray.length; i+= len) {
if (i + len > bArray.length)
len = bArray.length - i;
stream.write(bArray, i, len);
flushPipe(pipe);
}
performExec(pipe);
}
private static native long createPipe(int capacity);
private static native int pipeWriteFd(long p);
private static native void flushPipe(long p);
private static native void performExec(long p);
}

View File

@ -0,0 +1,226 @@
package com.termux.x11.starter;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.IIntentReceiver;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import java.lang.reflect.InvocationTargetException;
/**
* Wrapper around android.app.IActivityManager internal interface
*/
@SuppressLint("PrivateApi")
class IActivityManager {
private Object mAm;
private CrossVersionReflectedMethod mGetProviderMimeType;
private CrossVersionReflectedMethod mStartActivity;
/*
private CrossVersionReflectedMethod mBroadcastIntent;
*/
private CrossVersionReflectedMethod mStartServiceMethod;
private CrossVersionReflectedMethod mStopServiceMethod;
private CrossVersionReflectedMethod mGetIntentSenderMethod;
private CrossVersionReflectedMethod mIntentSenderSendMethod;
IActivityManager() throws Exception {
this("com.termux");
}
IActivityManager(String callingAppName) throws Exception {
try {
try {
mAm = android.app.ActivityManager.class
.getMethod("getService")
.invoke(null);
} catch (Exception e) {
mAm = Class.forName("android.app.ActivityManagerNative")
.getMethod("getDefault")
.invoke(null);
}
Class<?> amClass = mAm.getClass();
mGetProviderMimeType =
new CrossVersionReflectedMethod(amClass)
.tryMethodVariantInexact(
"getProviderMimeType",
Uri.class, "uri",
int.class, "userId"
);
mStartActivity =
new CrossVersionReflectedMethod(amClass)
.tryMethodVariantInexact(
"startActivityAsUser",
"android.app.IApplicationThread", "caller", null,
String.class, "callingPackage", callingAppName,
Intent.class, "intent", null,
String.class, "resolvedType", null,
IBinder.class, "resultTo", null,
String.class, "resultWho", null,
int.class, "requestCode", -1,
int.class, "flags", 0,
//ProfilerInfo profilerInfo, - let's autodetect
Bundle.class, "options", null,
int.class, "userId", 0
);
/*
mBroadcastIntent =
new CrossVersionReflectedMethod(amClass)
.tryMethodVariantInexact(
"broadcastIntent",
"android.app.IApplicationThread", "caller", null,
Intent.class, "intent", null,
String.class, "resolvedType", null,
IIntentReceiver.class, "resultTo", null,
int.class, "resultCode", -1,
String.class, "resultData", null,
Bundle.class, "map", null,
String[].class, "requiredPermissions", null,
int.class, "appOp", 0,
Bundle.class, "options", null,
boolean.class, "serialized", false,
boolean.class, "sticky", false,
int.class, "userId", 0
);
*/
mStartServiceMethod =
new CrossVersionReflectedMethod(amClass)
.tryMethodVariantInexact(
"startService",
"android.app.IApplicationThread", "caller", null,
Intent.class, "service", null,
String.class, "resolvedType", null,
boolean.class, "requireForeground", false,
String.class, "callingPackage", callingAppName,
int.class, "userId", 0
).tryMethodVariantInexact(
"startService",
"android.app.IApplicationThread", "caller", null,
Intent.class, "service", null,
String.class, "resolvedType", null,
String.class, "callingPackage", callingAppName,
int.class, "userId", 0
).tryMethodVariantInexact( // Pre frameworks/base 99b6043
"startService",
"android.app.IApplicationThread", "caller", null,
Intent.class, "service", null,
String.class, "resolvedType", null,
int.class, "userId", 0
);
mStopServiceMethod =
new CrossVersionReflectedMethod(amClass)
.tryMethodVariantInexact(
"stopService",
"android.app.IApplicationThread", "caller", null,
Intent.class, "service", null,
String.class, "resolvedType", null,
int.class, "userId", 0
);
mGetIntentSenderMethod =
new CrossVersionReflectedMethod(amClass)
.tryMethodVariantInexact(
"getIntentSender",
int.class, "type", 0,
String.class, "packageName", callingAppName,
IBinder.class, "token", null,
String.class, "resultWho", null,
int.class, "requestCode", 0,
Intent[].class, "intents", null,
String[].class, "resolvedTypes", null,
int.class, "flags", 0,
Bundle.class, "options", null,
int.class, "userId", 0
);
mIntentSenderSendMethod =
new CrossVersionReflectedMethod(Class.forName("android.content.IIntentSender"))
.tryMethodVariantInexact(
"send",
int.class, "code", 0,
Intent.class, "intent", null,
String.class, "resolvedType", null,
//IBinder.class, "android.os.IBinder whitelistToken", null,
"android.content.IIntentReceiver", "finishedReceiver", null,
String.class, "requiredPermission", null,
Bundle.class, "options", null
).tryMethodVariantInexact( // Pre frameworks/base a750a63
"send",
int.class, "code", 0,
Intent.class, "intent", null,
String.class, "resolvedType", null,
"android.content.IIntentReceiver", "finishedReceiver", null,
String.class, "requiredPermission", null
);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
int startActivityAsUser(Intent intent, String resolvedType, int flags, Bundle options, int userId) throws InvocationTargetException {
return (Integer) mStartActivity.invoke(
mAm,
"intent", intent,
"resolvedType", resolvedType,
"flags", flags,
"options", options,
"userId", userId
);
}
void broadcastIntent(Intent intent, IIntentReceiver resultTo, String[] requiredPermissions, boolean serialized, boolean sticky, int userId) throws InvocationTargetException {
/*
mBroadcastIntent.invoke(
mAm,
"intent", intent,
"resultTo", resultTo,
"requiredPermissions", requiredPermissions,
"serialized", serialized,
"sticky", sticky,
"userId", userId
);
*/
Object pendingIntent = mGetIntentSenderMethod.invoke(
mAm,
"type", 1 /*ActivityManager.INTENT_SENDER_BROADCAST*/,
"intents", new Intent[] { intent },
"flags", PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT,
"userId", userId
);
mIntentSenderSendMethod.invoke(
pendingIntent,
"requiredPermission", (requiredPermissions == null || requiredPermissions.length == 0) ? null : requiredPermissions[0],
"finishedReceiver", resultTo
);
}
String getProviderMimeType(Uri uri, int userId) throws InvocationTargetException {
return (String) mGetProviderMimeType.invoke(
mAm,
"uri", uri,
"userId", userId
);
}
ComponentName startService(Intent service, String resolvedType, int userId) throws InvocationTargetException {
return (ComponentName) mStartServiceMethod.invoke(
mAm,
"service", service,
"resolvedType", resolvedType,
"userId", userId
);
}
int stopService(Intent service, String resolvedType, int userId) throws InvocationTargetException {
return (Integer) mStopServiceMethod.invoke(
mAm,
"service", service,
"resolvedType", resolvedType,
"userId", userId
);
}
}

View File

@ -0,0 +1,309 @@
/*
**
** Copyright 2007, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
package com.termux.x11.starter;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.AndroidException;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Objects;
import com.termux.x11.common.ITermuxX11Internal;
@SuppressLint("UnsafeDynamicallyLoadedCode")
@SuppressWarnings({"unused", "RedundantThrows", "SameParameterValue", "FieldCanBeLocal"})
public class Starter {
@SuppressLint("SdCardPath")
private final String XwaylandPath = "/data/data/com.termux/files/usr/bin/Xwayland";
private final String TermuxX11ComponentName = "com.termux.x11/.TermuxX11StarterReceiver";
private String[] args;
private Service svc;
private ParcelFileDescriptor logFD;
/**
* Command-line entry point.
*
* @param args The command-line arguments
*/
public static void main(String[] args) {
try {
(new Thread(() -> {
try {
(new Starter()).onRun(args);
} catch (Throwable e) {
e.printStackTrace();
}
})).start();
synchronized (Thread.currentThread()) {
Looper.loop();
}
} catch (Throwable e) {
e.printStackTrace();
}
}
Runnable failedToStartActivity = () -> {
System.err.println("Android reported activity started but we did not get any respond");
System.err.println("Looks like we failed to start activity.");
System.err.println("Looks like Termux lacks \"Draw Over Apps\" permission.");
System.err.println("You can grant Termux the \"Draw Over Apps\" permission from its App Info activity:");
System.err.println("\tAndroid Settings -> Apps -> Termux -> Advanced -> Draw over other apps.");
};
public void onRun(String[] args) throws Throwable {
checkXdgRuntimeDir();
prepareLogFD();
if (checkWaylandSocket()) {
System.err.println("termux-x11 is already running");
startXwayland();
} else {
Starter.this.args = args;
svc = new Service();
handler.postDelayed(failedToStartActivity, 2000);
boolean launched = startActivity(svc);
}
}
public void onTransact(int code, Parcel data, Parcel reply, int flags) {
handler.removeCallbacks(failedToStartActivity);
}
private void prepareLogFD() {
logFD = ParcelFileDescriptor.adoptFd(openLogFD());
}
private ParcelFileDescriptor getWaylandFD() throws Throwable {
try {
ParcelFileDescriptor pfd;
int fd = createWaylandSocket();
if (fd == -1)
throw new Exception("Failed to bind a socket");
pfd = ParcelFileDescriptor.adoptFd(fd);
System.err.println(pfd);
return pfd;
} finally {
System.err.println("Lorie requested fd");
}
}
private void startXwayland() {
try {
boolean started = false;
for (int i = 0; i < 200; i++) {
if (checkWaylandSocket()) {
started = true;
Thread.sleep(200);
startXwayland(args);
break;
}
Thread.sleep(100);
}
if (!started)
System.err.println("Failed to connect to Termux:X11. Something went wrong.");
} catch (Exception e) {
e.printStackTrace();
}
exit(0);
}
private void onFinish() {
System.err.println("App sent finishing command");
startXwayland();
}
private boolean startActivity(IBinder token) throws Throwable {
Bundle bundle = new Bundle();
bundle.putBinder("", token);
Intent intent = new Intent();
intent.putExtra("com.termux.x11.starter", bundle);
ComponentName cn = ComponentName.unflattenFromString(TermuxX11ComponentName);
if (cn == null)
throw new IllegalArgumentException("Bad component name: " + TermuxX11ComponentName);
intent.setComponent(cn);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|
Intent.FLAG_ACTIVITY_SINGLE_TOP);
IActivityManager mAm;
try {
mAm = new IActivityManager();
} catch (Throwable e) {
e.printStackTrace();
throw new AndroidException("Can't connect to activity manager; is the system running?");
}
int res;
res = mAm.startActivityAsUser(intent,null, 0, null, 0);
PrintStream out = System.err;
boolean launched = false;
out.println("res = " + res);
switch (res) {
case ActivityManager.START_SUCCESS:
launched = true;
break;
case ActivityManager.START_SWITCHES_CANCELED:
launched = true;
out.println(
"Warning: Activity not started because the "
+ " current activity is being kept for the user.");
break;
case ActivityManager.START_DELIVERED_TO_TOP:
launched = true;
out.println(
"Warning: Activity not started, intent has "
+ "been delivered to currently running "
+ "top-most instance.");
break;
case ActivityManager.START_RETURN_INTENT_TO_CALLER:
launched = true;
out.println(
"Warning: Activity not started because intent "
+ "should be handled by the caller");
break;
case ActivityManager.START_TASK_TO_FRONT:
launched = true;
out.println(
"Warning: Activity not started, its current "
+ "task has been brought to the front");
break;
case ActivityManager.START_INTENT_NOT_RESOLVED:
out.println(
"Error: Activity not started, unable to "
+ "resolve " + intent.toString());
break;
case ActivityManager.START_CLASS_NOT_FOUND:
out.println("Error: Activity class " +
Objects.requireNonNull(intent.getComponent()).toShortString()
+ " does not exist.");
break;
case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
out.println(
"Error: Activity not started, you requested to "
+ "both forward and receive its result");
break;
case ActivityManager.START_PERMISSION_DENIED:
out.println(
"Error: Activity not started, you do not "
+ "have permission to access it.");
break;
case ActivityManager.START_NOT_VOICE_COMPATIBLE:
out.println(
"Error: Activity not started, voice control not allowed for: "
+ intent);
break;
case ActivityManager.START_NOT_CURRENT_USER_ACTIVITY:
out.println(
"Error: Not allowed to start background user activity"
+ " that shouldn't be displayed for all users.");
break;
default:
out.println(
"Error: Activity not started, unknown error code " + res);
break;
}
System.err.println("Activity is" + (launched?"":" not") + " started");
PendingIntent p;
return launched;
}
private void startXwayland(String[] args) throws IOException {
System.err.println("Starting Xwayland");
ExecHelper.exec(XwaylandPath, args);
}
class Service extends ITermuxX11Internal.Stub {
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws RemoteException {
Starter.this.onTransact(code, data, reply, flags);
return super.onTransact(code, data, reply, flags);
}
@Override
public ParcelFileDescriptor getWaylandFD() throws RemoteException {
System.err.println("Got getWaylandFD");
try {
return Starter.this.getWaylandFD();
} catch (Throwable e) {
throw new RemoteException(e.getMessage());
}
}
@Override
public ParcelFileDescriptor getLogFD() throws RemoteException {
System.err.println("Got getLogFD");
System.err.println(logFD);
return logFD;
}
@Override
public void finish() throws RemoteException {
System.err.println("Got finish request");
handler.postDelayed(Starter.this::onFinish, 10);
}
}
private void exit(int status) {
exit(50, status);
}
private void exit(int delay, int status) {
handler.postDelayed(() -> System.exit(status), delay);
}
private native void checkXdgRuntimeDir();
private native int createWaylandSocket();
private native boolean checkWaylandSocket();
private native int openLogFD();
@SuppressWarnings("FieldMayBeFinal")
private static Handler handler;
static {
Looper.prepare();
handler = new Handler();
@SuppressLint("SdCardPath")
final String libPath = "/data/data/com.termux/files/usr/libexec/termux-x11/libstarter.so";
final File libFile = new File(libPath);
if (libFile.exists()) {
Runtime.getRuntime().load(libPath);
} else {
System.err.println(libPath + " does not exist. Please, check termux-x11 package installation.");
System.exit(1);
}
}
}

View File

@ -0,0 +1,9 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := starter
LOCAL_SRC_FILES := starter.c exechelper.c
LOCAL_LDLIBS := -llog -ldl
include $(BUILD_SHARED_LIBRARY)
include $(LOCAL_PATH)/../../../../common/src/main/jni/Android.mk

View File

@ -0,0 +1,124 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "cppcoreguidelines-narrowing-conversions"
#pragma ide diagnostic ignored "hicpp-signed-bitwise"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <jni.h>
#include <fcntl.h>
#define unused __attribute__((__unused__))
void DumpHex(const void* data, size_t size);
typedef struct {
int fds[2];
int position;
uint8_t data[0];
} nativePipe;
static long toLong(nativePipe* p) {
union {
jlong l;
nativePipe* p;
} u = {0};
u.p = p;
return u.l;
}
static nativePipe* fromLong(jlong v) {
union {
jlong l;
nativePipe* p;
} u = {0};
u.l = v;
return u.p;
}
JNIEXPORT jlong JNICALL
Java_com_termux_x11_starter_ExecHelper_createPipe(unused JNIEnv *env, unused jclass clazz,
jint capacity) {
size_t size = sizeof(nativePipe) + sizeof(uint8_t) * capacity + 1;
nativePipe* p = malloc(size);
memset(p, 0, size);
if (p != NULL) pipe(p->fds);
int flags = fcntl(p->fds[0], F_GETFL, 0);
fcntl(p->fds[0], F_SETFL, flags | O_NONBLOCK);
flags = fcntl(p->fds[1], F_GETFL, 0);
fcntl(p->fds[1], F_SETFL, flags | O_NONBLOCK);
return toLong(p);
}
JNIEXPORT jint JNICALL
Java_com_termux_x11_starter_ExecHelper_pipeWriteFd(unused JNIEnv *env, unused jclass clazz,
jlong jp) {
nativePipe* p = fromLong(jp);
if (!p)
return -1;
return p->fds[1];
}
JNIEXPORT void JNICALL
Java_com_termux_x11_starter_ExecHelper_flushPipe(unused JNIEnv *env, unused jclass clazz, jlong jp) {
nativePipe* p = fromLong(jp);
if (!p)
return;
int bytesRead = read(p->fds[0], p->data + p->position, 1024);
p->position += bytesRead;
}
JNIEXPORT void JNICALL
Java_com_termux_x11_starter_ExecHelper_performExec(unused JNIEnv *env, unused jclass clazz, jlong jp) {
nativePipe* p = fromLong(jp);
if (!p)
return;
//DumpHex(p->data, p->position);
char *argv[1024] = {0};
argv[0] = (char*) p->data;
for (int i = 0, j = 1; i < p->position; i++) {
if (p->data[i] == 0 && i+1 < p->position) {;
argv[j++] = (char*) p->data + i + 1;
}
}
for (int i=0; i<1024; i++) {
if (argv[i]) {
printf("argv[%d] = %s \n", i, argv[i]);
DumpHex(argv[i], strlen(argv[i]));
}
}
execv(argv[0], argv);
perror("execv");
exit (1);
}
void DumpHex(const void* data, size_t size) {
char ascii[17];
size_t i, j;
ascii[16] = '\0';
for (i = 0; i < size; ++i) {
printf("%02X ", ((unsigned char*)data)[i]);
if (((unsigned char*)data)[i] >= ' ' && ((unsigned char*)data)[i] <= '~') {
ascii[i % 16] = ((unsigned char*)data)[i];
} else {
ascii[i % 16] = '.';
}
if ((i+1) % 8 == 0 || i+1 == size) {
printf(" ");
if ((i+1) % 16 == 0) {
printf("| %s \n", ascii);
} else if (i+1 == size) {
ascii[(i+1) % 16] = '\0';
if ((i+1) % 16 <= 8) {
printf(" ");
}
for (j = (i+1) % 16; j < 16; ++j) {
printf(" ");
}
printf("| %s \n", ascii);
}
}
}
}
#pragma clang diagnostic pop

View File

@ -0,0 +1,158 @@
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/poll.h>
#include <sys/un.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <dlfcn.h>
#include <sys/stat.h>
#define unused __attribute__((__unused__))
#define DEFAULT_PREFIX "/data/data/com.termux/files/usr"
#define DEFAULT_XDG_RUNTIME_DIR DEFAULT_PREFIX "/tmp"
#define DEFAULT_SOCKET_NAME "wayland-0"
static int socket_action(int* fd, char* path,
int (*action)(int, const struct sockaddr *, socklen_t)) {
if (!fd || !action) {
errno = -EINVAL;
return -1;
}
struct sockaddr_un local;
size_t len;
if((*fd = socket(PF_LOCAL, SOCK_STREAM, 0)) == -1)
{
perror("socket");
return *fd;
}
local.sun_family = AF_UNIX;
strcpy(local.sun_path, path);
len = strlen(local.sun_path) + sizeof(local.sun_family);
return action(*fd, (struct sockaddr *)&local, len);
}
JNIEXPORT void JNICALL
Java_com_termux_x11_starter_Starter_checkXdgRuntimeDir(unused JNIEnv *env, unused jobject thiz) {
char* XDG_RUNTIME_DIR = getenv("XDG_RUNTIME_DIR");
if (!XDG_RUNTIME_DIR || strlen(XDG_RUNTIME_DIR) == 0) {
printf("$XDG_RUNTIME_DIR is unset.\n");
printf("Exporting default value (%s).\n", DEFAULT_XDG_RUNTIME_DIR);
setenv("XDG_RUNTIME_DIR", DEFAULT_XDG_RUNTIME_DIR, true);
}
}
static const char *getWaylandSocketPath() {
static char* path = NULL;
if (path != NULL)
return path;
path = malloc(256 * sizeof(char));
memset(path, 0, 256 * sizeof(char));
char* XDG_RUNTIME_DIR = getenv("XDG_RUNTIME_DIR");
if (!XDG_RUNTIME_DIR || strlen(XDG_RUNTIME_DIR) == 0) {
printf("$XDG_RUNTIME_DIR is unset");
exit(1);
}
sprintf(path, "%s/%s", XDG_RUNTIME_DIR, DEFAULT_SOCKET_NAME);
return path;
}
JNIEXPORT jboolean JNICALL
Java_com_termux_x11_starter_Starter_checkWaylandSocket(unused JNIEnv *env, unused jobject thiz) {
int fd;
errno = 0;
if (socket_action(&fd, (char *) getWaylandSocketPath(), connect) != -1) {
close(fd);
return 1;
}
return 0;
}
JNIEXPORT jint JNICALL
Java_com_termux_x11_starter_Starter_createWaylandSocket(unused JNIEnv *env, unused jobject thiz) {
int fd;
errno = 0;
unlink(getWaylandSocketPath());
if (socket_action(&fd, (char *) getWaylandSocketPath(), bind) < 0) {
perror("socket");
return -1;
}
return fd;
}
#pragma clang diagnostic push
#pragma ide diagnostic ignored "hicpp-signed-bitwise"
JNIEXPORT jint JNICALL
Java_com_termux_x11_starter_Starter_openLogFD(unused JNIEnv *env, unused jobject thiz) {
const char* TERMUX_X11_LOG_FILE = getenv("TERMUX_X11_LOG_FILE");
int sv[2]; /* the pair of socket descriptors */
if (TERMUX_X11_LOG_FILE == NULL || strlen(TERMUX_X11_LOG_FILE) == 0)
return -1;
int logfd = open(TERMUX_X11_LOG_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (logfd < 0) {
perror("open logfile");
return -1;
}
if (pipe(sv) == -1) {
perror("pipe");
return -1;
}
fchmod (sv[0], 0777);
fchmod (sv[1], 0777);
switch(fork()) {
/*
* Android do not allow another process to write to tty or pipe fd of our process.
* We can not force allowing even using chmod.
* That is a reason we are using pipe and cat here.
*/
case 0: {
int new_stderr = dup(2);
close(sv[1]);
dup2(sv[0], 0);
dup2(logfd, 1);
dup2(logfd, 2);
close(sv[0]);
close(logfd);
printf("cat started (%d)\n", getpid());
struct pollfd pfd = {0};
pfd.fd = 0;
pfd.events = POLLIN | POLLHUP;
poll(&pfd, 1, 10000);
execl("/data/data/com.termux/files/usr/bin/cat", "cat", NULL);
dprintf(new_stderr, "execl cat: %s\n", strerror(errno));
return -1;
}
case -1:
return -1;
default:
close(sv[0]);
close(logfd);
return sv[1];
}
}
#pragma clang diagnostic pop

24
termux-startx11 Executable file
View File

@ -0,0 +1,24 @@
#!/data/data/com.termux/files/usr/bin/bash
DISPLAYNO=0
socket_is_open() {
local SOCKET=$PREFIX/tmp/.X11-unix/X$DISPLAYNO
local NULL=/dev/null
socat -u OPEN:$NULL UNIX-CONNECT:$SOCKET > $NULL 2>&1
return $?
}
/data/data/com.termux/files/usr/bin/termux-x11 :$DISPLAYNO &
{
#kill this script if termux-x11 is not up after 10 seconds
sleep 10
kill $$
} &
until socket_is_open; do
sleep 0.1
done
export DISPLAY=:$DISPLAYNO
exec x-session-manager

4
termux-x11 Executable file
View File

@ -0,0 +1,4 @@
#!/data/data/com.termux/files/usr/bin/sh
export CLASSPATH=/data/data/com.termux/files/usr/libexec/termux-x11/starter.apk
unset LD_LIBRARY_PATH LD_PRELOAD
exec /system/bin/app_process / com.termux.x11.starter.Starter "$@"