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