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:
parent
db575f7887
commit
f9a9ce3167
3
.github/workflows/debug_build.yml
vendored
3
.github/workflows/debug_build.yml
vendored
@ -17,6 +17,9 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
./gradlew assembleDebug
|
./gradlew assembleDebug
|
||||||
|
- name: Build companion package
|
||||||
|
run: |
|
||||||
|
./build_termux_package
|
||||||
- name: Store generated APK file
|
- name: Store generated APK file
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -36,3 +36,6 @@ Thumbs.db
|
|||||||
|
|
||||||
#files containing github tokens
|
#files containing github tokens
|
||||||
github.properties
|
github.properties
|
||||||
|
|
||||||
|
*.cxx
|
||||||
|
app/.cxx/*
|
||||||
|
81
README.md
81
README.md
@ -1,25 +1,25 @@
|
|||||||
|
|
||||||
# Termux:X11
|
# Termux:X11
|
||||||
|
|
||||||
[![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux)
|
[![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.
|
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
|
## 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?
|
## 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
|
## 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
|
## 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:
|
then you can start your desired graphical application by doing:
|
||||||
```
|
```
|
||||||
~ $ export XDG_RUNTIME_DIR=${TMPDIR}
|
~ $ export XDG_RUNTIME_DIR=${TMPDIR}
|
||||||
~ $ Xwayland :1 >/dev/null &
|
~ $ termux-x11 :1 >/dev/null &
|
||||||
~ $ env DISPLAY=:1 xfce4-session
|
~ $ env DISPLAY=:1 xfce4-session
|
||||||
```
|
```
|
||||||
You may replace `xfce4-session` if you use other than Xfce
|
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)
|
![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
|
# License
|
||||||
Released under the [GPLv3 license](https://www.gnu.org/licenses/gpl-3.0.html).
|
Released under the [GPLv3 license](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||||
|
@ -5,9 +5,10 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.termux.x11"
|
applicationId "com.termux.x11"
|
||||||
minSdkVersion 24
|
minSdkVersion 24
|
||||||
|
//noinspection OldTargetApi
|
||||||
targetSdkVersion 28
|
targetSdkVersion 28
|
||||||
versionCode 7
|
versionCode 9
|
||||||
versionName "1.02.04"
|
versionName "1.02.06"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@ -30,6 +31,11 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
ndkBuild {
|
ndkBuild {
|
||||||
path "src/main/jni/Android.mk"
|
path "src/main/jni/Android.mk"
|
||||||
@ -47,6 +53,7 @@ dependencies {
|
|||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
||||||
|
implementation project(path: ':common')
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
|
@ -3,8 +3,9 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.termux.x11"
|
package="com.termux.x11"
|
||||||
android:installLocation="internalOnly"
|
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.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"
|
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"
|
||||||
@ -18,16 +19,11 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
tools:ignore="GoogleAppIndexingWarning"
|
tools:ignore="GoogleAppIndexingWarning"
|
||||||
android:debuggable="true">
|
>
|
||||||
<service
|
<service
|
||||||
android:name=".LorieService"
|
android:name=".LorieService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
<service
|
|
||||||
android:name=".LorieTestService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:process="com.termux.x11.separated"/>
|
|
||||||
<activity android:name=".MainActivity"
|
<activity android:name=".MainActivity"
|
||||||
android:theme="@style/NoActionBar"
|
android:theme="@style/NoActionBar"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
@ -50,8 +46,13 @@
|
|||||||
android:supportsPictureInPicture="false"
|
android:supportsPictureInPicture="false"
|
||||||
android:resizeableActivity="true">
|
android:resizeableActivity="true">
|
||||||
</activity>
|
</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="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
|
||||||
<meta-data android:name="android.allow_multiple_resumed_activities" android:value="true" />
|
<meta-data android:name="android.allow_multiple_resumed_activities" android:value="true" />
|
||||||
</application>
|
</application>
|
||||||
|
<queries>
|
||||||
|
<package android:name="com.termux" />
|
||||||
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -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
BIN
app/src/main/assets/X11.zip
Normal file
Binary file not shown.
@ -2,6 +2,7 @@ package com.termux.x11;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.ActivityManager;
|
import android.app.ActivityManager;
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
@ -10,15 +11,22 @@ import android.app.Service;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@ -30,13 +38,21 @@ import android.view.SurfaceHolder;
|
|||||||
import android.view.SurfaceView;
|
import android.view.SurfaceView;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.Toast;
|
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"})
|
@SuppressLint({"ClickableViewAccessibility", "StaticFieldLeak"})
|
||||||
public class LorieService extends Service {
|
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_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_FROM_ACTIVITY = "com.termux.x11.start_from_activity";
|
||||||
static final String ACTION_START_PREFERENCES_ACTIVITY = "com.termux.x11.start_preferences_activity";
|
static final String ACTION_START_PREFERENCES_ACTIVITY = "com.termux.x11.start_preferences_activity";
|
||||||
@ -46,7 +62,7 @@ public class LorieService extends Service {
|
|||||||
//private
|
//private
|
||||||
//static
|
//static
|
||||||
long compositor;
|
long compositor;
|
||||||
private static ServiceEventListener listener = new ServiceEventListener();
|
private static final ServiceEventListener listener = new ServiceEventListener();
|
||||||
private static MainActivity act;
|
private static MainActivity act;
|
||||||
|
|
||||||
private TouchParser mTP;
|
private TouchParser mTP;
|
||||||
@ -70,14 +86,33 @@ public class LorieService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("BatteryLife")
|
@SuppressLint({"BatteryLife", "ObsoleteSdkInt"})
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
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/");
|
if (isServiceRunningInForeground(this, LorieService.class)) return;
|
||||||
compositor = createLorieThread(Xdgpath);
|
|
||||||
|
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) {
|
if (compositor == 0) {
|
||||||
Log.e("LorieService", "compositor thread was not created");
|
Log.e("LorieService", "compositor thread was not created");
|
||||||
@ -189,6 +224,49 @@ public class LorieService extends Service {
|
|||||||
return START_REDELIVER_INTENT;
|
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
|
@Override
|
||||||
public void onDestroy() {
|
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 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);
|
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 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 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);
|
private native void terminate(long compositor);
|
||||||
|
|
||||||
|
public static native void startLogcatForFd(int fd);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
System.loadLibrary("lorie");
|
System.loadLibrary("lorie");
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,13 @@
|
|||||||
package com.termux.x11;
|
package com.termux.x11;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
@ -14,7 +17,6 @@ import android.view.View;
|
|||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.Toast;
|
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
@ -53,11 +55,16 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
getWindow().
|
getWindow().
|
||||||
getDecorView().
|
getDecorView().
|
||||||
setPointerIcon(PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL));
|
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;
|
int orientation;
|
||||||
@Override
|
@Override
|
||||||
public void onConfigurationChanged(Configuration newConfig) {
|
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||||
super.onConfigurationChanged(newConfig);
|
super.onConfigurationChanged(newConfig);
|
||||||
|
|
||||||
if (newConfig.orientation != orientation && kbd != null && kbd.getVisibility() == View.VISIBLE) {
|
if (newConfig.orientation != orientation && kbd != null && kbd.getVisibility() == View.VISIBLE) {
|
||||||
@ -85,7 +92,6 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
super.onWindowFocusChanged(hasFocus);
|
super.onWindowFocusChanged(hasFocus);
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
Window window = getWindow();
|
Window window = getWindow();
|
||||||
View decorView = window.getDecorView();
|
|
||||||
|
|
||||||
if (preferences.getBoolean("Reseed", true))
|
if (preferences.getBoolean("Reseed", true))
|
||||||
{
|
{
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
ROOT_PATH := $(call my-dir)
|
ROOT_PATH := $(call my-dir)
|
||||||
include $(ROOT_PATH)/libxkbcommon/Android.mk
|
include $(ROOT_PATH)/libxkbcommon/Android.mk
|
||||||
include $(ROOT_PATH)/lorie/Android.mk
|
include $(ROOT_PATH)/lorie/Android.mk
|
||||||
|
include $(ROOT_PATH)/../../../../common/src/main/jni/Android.mk
|
||||||
|
@ -37,11 +37,11 @@ LOCAL_SRC_FILES := \
|
|||||||
LOCAL_CFLAGS := \
|
LOCAL_CFLAGS := \
|
||||||
-std=c99 -Wall -Werror -Wno-unused-parameter -Wno-missing-field-initializers -Wimplicit-function-declaration \
|
-std=c99 -Wall -Werror -Wno-unused-parameter -Wno-missing-field-initializers -Wimplicit-function-declaration \
|
||||||
-D_GNU_SOURCE \
|
-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_LAYOUT=\"us\" \
|
||||||
-DDEFAULT_XKB_MODEL=\"pc105\" \
|
-DDEFAULT_XKB_MODEL=\"pc105\" \
|
||||||
-DDEFAULT_XKB_RULES=\"evdev\" \
|
-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_C_INCLUDES := $(LOCAL_PATH)/xkbcommon $(LOCAL_PATH)/xkbcommon/src $(LOCAL_PATH)/xkbcommon/include
|
||||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/xkbcommon/
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/xkbcommon/
|
||||||
include $(BUILD_SHARED_LIBRARY)
|
include $(BUILD_SHARED_LIBRARY)
|
||||||
|
@ -11,6 +11,10 @@
|
|||||||
#include <lorie-egl-helper.hpp>
|
#include <lorie-egl-helper.hpp>
|
||||||
#include <android/native_window_jni.h>
|
#include <android/native_window_jni.h>
|
||||||
#include <xkbcommon/xkbcommon.h>
|
#include <xkbcommon/xkbcommon.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
|
||||||
|
#define unused __attribute__((__unused__))
|
||||||
|
|
||||||
#define DEFAULT_WIDTH 480
|
#define DEFAULT_WIDTH 480
|
||||||
#define DEFAULT_HEIGHT 800
|
#define DEFAULT_HEIGHT 800
|
||||||
@ -26,6 +30,7 @@ public:
|
|||||||
void get_keymap(int *fd, int *size) override;
|
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 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 layout_change_callback(char *layout);
|
||||||
|
void passfd(int fd);
|
||||||
|
|
||||||
void on_egl_init();
|
void on_egl_init();
|
||||||
void on_egl_uninit();
|
void on_egl_uninit();
|
||||||
@ -160,6 +165,10 @@ void LorieBackendAndroid::layout_change_callback(char *layout) {
|
|||||||
|
|
||||||
keyboard_keymap_changed();
|
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
|
extern "C" JNIEXPORT jlong JNICALL
|
||||||
JNI_DECLARE(LorieService, createLorieThread)(JNIEnv *env, jobject __unused instance, jstring CustXdgpath) {
|
JNI_DECLARE(LorieService, createLorieThread)(unused JNIEnv *env, unused jobject instance) {
|
||||||
|
|
||||||
const char *pathx = env->GetStringUTFChars(CustXdgpath, NULL);
|
|
||||||
|
|
||||||
setenv("XDG_RUNTIME_DIR", pathx, 1);
|
|
||||||
return (jlong) new LorieBackendAndroid;
|
return (jlong) new LorieBackendAndroid;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" JNIEXPORT void JNICALL
|
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;
|
if (jcompositor == 0) return;
|
||||||
LorieBackendAndroid *b = fromLong(jcompositor);
|
LorieBackendAndroid *b = fromLong(jcompositor);
|
||||||
LOGI("JNI: requested termination");
|
LOGI("JNI: requested termination");
|
||||||
@ -198,7 +216,7 @@ JNI_DECLARE(LorieService, terminate)(JNIEnv __unused *env, jobject __unused inst
|
|||||||
}
|
}
|
||||||
|
|
||||||
extern "C" JNIEXPORT void JNICALL
|
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;
|
if (jcompositor == 0) return;
|
||||||
LorieBackendAndroid *b = fromLong(jcompositor);
|
LorieBackendAndroid *b = fromLong(jcompositor);
|
||||||
|
|
||||||
@ -209,7 +227,7 @@ JNI_DECLARE(LorieService, windowChanged)(JNIEnv *env, jobject __unused instance,
|
|||||||
}
|
}
|
||||||
|
|
||||||
extern "C" JNIEXPORT void JNICALL
|
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;
|
if (jcompositor == 0) return;
|
||||||
LorieBackendAndroid *b = fromLong(jcompositor);
|
LorieBackendAndroid *b = fromLong(jcompositor);
|
||||||
LOGV("JNI: touch down");
|
LOGV("JNI: touch down");
|
||||||
@ -218,7 +236,7 @@ JNI_DECLARE(LorieService, touchDown)(JNIEnv __unused *env, jobject __unused inst
|
|||||||
}
|
}
|
||||||
|
|
||||||
extern "C" JNIEXPORT void JNICALL
|
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;
|
if (jcompositor == 0) return;
|
||||||
LorieBackendAndroid *b = fromLong(jcompositor);
|
LorieBackendAndroid *b = fromLong(jcompositor);
|
||||||
LOGV("JNI: touch motion");
|
LOGV("JNI: touch motion");
|
||||||
@ -227,7 +245,7 @@ JNI_DECLARE(LorieService, touchMotion)(JNIEnv __unused *env, jobject __unused in
|
|||||||
}
|
}
|
||||||
|
|
||||||
extern "C" JNIEXPORT void JNICALL
|
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;
|
if (jcompositor == 0) return;
|
||||||
LorieBackendAndroid *b = fromLong(jcompositor);
|
LorieBackendAndroid *b = fromLong(jcompositor);
|
||||||
LOGV("JNI: touch up");
|
LOGV("JNI: touch up");
|
||||||
@ -236,7 +254,7 @@ JNI_DECLARE(LorieService, touchUp)(JNIEnv __unused *env, jobject __unused instan
|
|||||||
}
|
}
|
||||||
|
|
||||||
extern "C" JNIEXPORT void JNICALL
|
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;
|
if (jcompositor == 0) return;
|
||||||
LorieBackendAndroid *b = fromLong(jcompositor);
|
LorieBackendAndroid *b = fromLong(jcompositor);
|
||||||
LOGV("JNI: touch frame");
|
LOGV("JNI: touch frame");
|
||||||
@ -245,7 +263,7 @@ JNI_DECLARE(LorieService, touchFrame)(JNIEnv __unused *env, jobject __unused ins
|
|||||||
}
|
}
|
||||||
|
|
||||||
extern "C" JNIEXPORT void JNICALL
|
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;
|
if (jcompositor == 0) return;
|
||||||
LorieBackendAndroid *b = fromLong(jcompositor);
|
LorieBackendAndroid *b = fromLong(jcompositor);
|
||||||
|
|
||||||
@ -254,7 +272,7 @@ JNI_DECLARE(LorieService, pointerMotion)(JNIEnv __unused *env, jobject __unused
|
|||||||
}
|
}
|
||||||
|
|
||||||
extern "C" JNIEXPORT void JNICALL
|
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;
|
if (jcompositor == 0) return;
|
||||||
LorieBackendAndroid *b = fromLong(jcompositor);
|
LorieBackendAndroid *b = fromLong(jcompositor);
|
||||||
|
|
||||||
@ -263,7 +281,7 @@ JNI_DECLARE(LorieService, pointerScroll)(JNIEnv __unused *env, jobject __unused
|
|||||||
}
|
}
|
||||||
|
|
||||||
extern "C" JNIEXPORT void JNICALL
|
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;
|
if (jcompositor == 0) return;
|
||||||
LorieBackendAndroid *b = fromLong(jcompositor);
|
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" void android_keycode_get_eventcode(int kc, int *ec, int *shift);
|
||||||
|
|
||||||
extern "C" JNIEXPORT void JNICALL
|
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,
|
jlong jcompositor, jint type,
|
||||||
jint key_code, jint jshift,
|
jint key_code, jint jshift,
|
||||||
jstring characters_) {
|
jstring characters_) {
|
||||||
@ -325,3 +343,66 @@ JNI_DECLARE(LorieService, keyboardKey)(JNIEnv *env, jobject __unused instance,
|
|||||||
|
|
||||||
if (characters_ != NULL) env->ReleaseStringUTFChars(characters_, characters);
|
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
61
build_termux_package
Executable 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
1
common/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
38
common/build.gradle
Normal file
38
common/build.gradle
Normal 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'
|
||||||
|
}
|
0
common/consumer-rules.pro
Normal file
0
common/consumer-rules.pro
Normal file
21
common/proguard-rules.pro
vendored
Normal file
21
common/proguard-rules.pro
vendored
Normal 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
|
5
common/src/main/AndroidManifest.xml
Normal file
5
common/src/main/AndroidManifest.xml
Normal 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>
|
@ -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();
|
||||||
|
}
|
1
common/src/main/jni/Android.mk
Normal file
1
common/src/main/jni/Android.mk
Normal file
@ -0,0 +1 @@
|
|||||||
|
include $(call all-subdir-makefiles)
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
|||||||
|
#Thu Sep 09 20:28:30 IDT 2021
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=a8da5b02437a60819cad23e10fc7e9cf32bcb57029d9cb277e26eeff76ce014b
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
||||||
|
@ -1 +1,3 @@
|
|||||||
|
include ':starter'
|
||||||
|
include ':common'
|
||||||
include ':app'
|
include ':app'
|
||||||
|
1
starter/.gitignore
vendored
Normal file
1
starter/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
44
starter/build.gradle
Normal file
44
starter/build.gradle
Normal 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
21
starter/proguard-rules.pro
vendored
Normal 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
|
7
starter/src/main/AndroidManifest.xml
Normal file
7
starter/src/main/AndroidManifest.xml
Normal 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>
|
13
starter/src/main/java/android/content/IIntentReceiver.java
Normal file
13
starter/src/main/java/android/content/IIntentReceiver.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
45
starter/src/main/java/com/termux/x11/starter/ExecHelper.java
Normal file
45
starter/src/main/java/com/termux/x11/starter/ExecHelper.java
Normal 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);
|
||||||
|
}
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
309
starter/src/main/java/com/termux/x11/starter/Starter.java
Normal file
309
starter/src/main/java/com/termux/x11/starter/Starter.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
9
starter/src/main/jni/Android.mk
Normal file
9
starter/src/main/jni/Android.mk
Normal 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
|
124
starter/src/main/jni/exechelper.c
Normal file
124
starter/src/main/jni/exechelper.c
Normal 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
|
158
starter/src/main/jni/starter.c
Normal file
158
starter/src/main/jni/starter.c
Normal 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
24
termux-startx11
Executable 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
4
termux-x11
Executable 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 "$@"
|
Loading…
Reference in New Issue
Block a user