diff --git a/.github/workflows/build-anland-apk.yml b/.github/workflows/build-anland-apk.yml index 5847e88..aaded5c 100644 --- a/.github/workflows/build-anland-apk.yml +++ b/.github/workflows/build-anland-apk.yml @@ -6,6 +6,10 @@ on: - termux paths: - "app/**" + - "packages/anland/**" + - "termux/**" + - "tools/**" + - ".github/workflows/build-anland-apk.yml" workflow_dispatch: inputs: ref: @@ -99,16 +103,35 @@ jobs: run: | set -euo pipefail tools/build-app.sh + tools/build-compatible-app.sh - - name: Calculate checksums + - name: Calculate checksums for compatible APK shell: bash run: | set -euo pipefail cd out + sha256sum *compatible.apk | sort -k2 > sha256sums.txt + cat sha256sums.txt + + - name: Upload compatible APK artifacts + uses: actions/upload-artifact@v7 + with: + name: anland-termux-apk-${{ github.run_id }}-${{ github.run_attempt }}-compatible + path: | + out/*compatible.apk + out/sha256sums.txt + retention-days: 90 + + - name: Calculate checksums for standard APK + shell: bash + run: | + set -euo pipefail + cd out + rm -f *compatible.apk sha256sums.txt sha256sum *.apk | sort -k2 > sha256sums.txt cat sha256sums.txt - - name: Upload APK artifacts + - name: Upload standard APK artifacts uses: actions/upload-artifact@v7 with: name: anland-termux-apk-${{ github.run_id }}-${{ github.run_attempt }} diff --git a/README.md b/README.md index af2d309..5ca2cec 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,11 @@ Use [Anland](https://github.com/superturtlee/anland) in Termux, including **Term * Other processors, such as MediaTek Dimensity, Google Tensor, and Samsung Exynos, can currently use only software rendering (LLVMpipe) and may not be able to enter the Wayland desktop. So far, only the Google Tensor G1 has been tested by me; it can successfully enter the desktop in PRoot or Chroot containers. For other processors, you can also follow the instructions in the User Guide to have a try. **It would be helpful to share how this project works on your device in [Issues](https://github.com/lfdevs/anland-termux/issues).** +### Termux sources and variants + +* If Termux comes from the [official GitHub repository](https://github.com/termux/termux-app/releases), use `AnlandTermux-.apk`. It uses `sharedUserId` and runs as part of Termux, so Android always treats them as the same app without reducing performance. +* If Termux comes from [F-Droid](https://f-droid.org/packages/com.termux/) or a variant such as ZeroTermux, use `AnlandTermux--compatible.apk` instead. It does not use `sharedUserId`; a Termux-side `anland-compatible` bridge holds the daemon socket and transfers its fd through Binder. See the [user guide](docs/user-guide.md) for details. + ## User Guide For instructions on installing and using Anland: Termux, see [Anland: Termux User Guide](docs/user-guide.md). diff --git a/README_zh.md b/README_zh.md index ed81b55..ab5f411 100644 --- a/README_zh.md +++ b/README_zh.md @@ -28,6 +28,11 @@ * 其他处理器(如联发科天玑、Google Tensor、三星猎户座等)目前只能使用软件渲染(即 LLVMpipe),而且不一定能成功进入 Wayland 桌面。目前我只测试了 Google Tensor G1,它可以在 PRoot 或 Chroot 容器里成功进入桌面。对于其他处理器,你可以按照“用户指南”一节的说明先行尝试。 **欢迎在 [Issues](https://github.com/lfdevs/anland-termux/issues) 里反馈本项目在你的设备上的运行情况。** +### Termux 来源及变体 + +* 如果 Termux 来自 [GitHub 官方仓库](https://github.com/termux/termux-app/releases),请使用 `AnlandTermux-.apk`。它使用 `sharedUserId` 且作为 Termux 的一部分运行,因此 Android 会一直将它们视为同一个应用,而且不会降低其速度。 +* 如果 Termux 来自 [F-Droid](https://f-droid.org/packages/com.termux/) 或 ZeroTermux 等变体,请改用 `AnlandTermux--compatible.apk`。它不使用 `sharedUserId`,而是由 Termux 侧的 `anland-compatible` bridge 持有守护程序 socket,再通过 Binder 传递 fd。详情请参阅[用户指南](docs/user-guide_zh.md)。 + ## 用户指南 Anland: Termux 的安装和使用说明请参见:[Anland:Termux 用户指南](docs/user-guide_zh.md) diff --git a/app/build.gradle b/app/build.gradle index 8e9b0a6..7a560f0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,6 +8,7 @@ android { buildFeatures { buildConfig = true + aidl = true } signingConfigs { @@ -37,6 +38,19 @@ android { } } + flavorDimensions "transport" + productFlavors { + standard { + dimension "transport" + buildConfigField "boolean", "COMPATIBLE", "false" + } + compatible { + dimension "transport" + buildConfigField "boolean", "COMPATIBLE", "true" + versionNameSuffix "-compatible" + } + } + ndkVersion = "29.0.14206865" externalNativeBuild { diff --git a/app/src/compatible/AndroidManifest.xml b/app/src/compatible/AndroidManifest.xml new file mode 100644 index 0000000..b2d3ea1 --- /dev/null +++ b/app/src/compatible/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b007d2e..fb554a1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + diff --git a/app/src/main/aidl/com/anland/termux/ICompatibleBridge.aidl b/app/src/main/aidl/com/anland/termux/ICompatibleBridge.aidl new file mode 100644 index 0000000..44540bd --- /dev/null +++ b/app/src/main/aidl/com/anland/termux/ICompatibleBridge.aidl @@ -0,0 +1,7 @@ +package com.anland.termux; + +import android.os.ParcelFileDescriptor; + +interface ICompatibleBridge { + ParcelFileDescriptor getConnection(); +} diff --git a/app/src/main/java/com/anland/termux/CompatibleBridge.java b/app/src/main/java/com/anland/termux/CompatibleBridge.java new file mode 100644 index 0000000..0513437 --- /dev/null +++ b/app/src/main/java/com/anland/termux/CompatibleBridge.java @@ -0,0 +1,147 @@ +package com.anland.termux; + +import android.content.Context; +import android.content.Intent; +import android.net.LocalSocket; +import android.net.LocalSocketAddress; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.IOException; +import java.lang.reflect.Method; + +/** + * Termux-side entry point for the APK without sharedUserId. + * + * The command installed by the Anland Termux package starts this class with + * app_process. It connects to the daemon as the Termux UID and publishes a + * Binder which lets the Android app duplicate that connected socket fd. + */ +public final class CompatibleBridge { + public static final String ACTION_START = "com.anland.termux.COMPATIBLE_BRIDGE_START"; + + private static final String TAG = "AnlandCompatibleBridge"; + private static final String APPLICATION_ID = "com.anland.termux"; + + private final Context context; + private final BridgeBinder binder; + + private CompatibleBridge(String socketPath) throws IOException { + context = createContext(); + if (context == null) + throw new IOException("could not create an Android system context"); + binder = new BridgeBinder(socketPath); + } + + public static void main(String[] args) { + if (Looper.getMainLooper() == null) + Looper.prepareMainLooper(); + + String socketPath = args.length == 1 ? args[0] : null; + if (socketPath == null || socketPath.isEmpty()) { + System.err.println("usage: CompatibleBridge SOCKET_PATH"); + return; + } + + try { + new CompatibleBridge(socketPath).run(); + } catch (Exception e) { + Log.e(TAG, "compatible bridge failed", e); + e.printStackTrace(System.err); + } + } + + private void run() { + Handler handler = new Handler(Looper.getMainLooper()); + Runnable broadcaster = new Runnable() { + @Override + public void run() { + // Keep publishing the Binder so a recreated Activity can attach to + // the already-running Termux-side bridge without starting another + // process. MainActivity ignores duplicate broadcasts while its fd + // is still in use. + sendBroadcast(); + handler.postDelayed(this, 1000L); + } + }; + handler.post(broadcaster); + Looper.loop(); + } + + private void sendBroadcast() { + Bundle bundle = new Bundle(); + bundle.putBinder(null, binder); + + Intent intent = new Intent(ACTION_START); + intent.putExtra(null, bundle); + intent.setPackage(APPLICATION_ID); + try { + context.sendBroadcast(intent); + } catch (RuntimeException e) { + Log.e(TAG, "failed to send compatible bridge broadcast", e); + } + } + + private static Context createContext() { + try { + Class activityThreadClass = Class.forName("android.app.ActivityThread"); + Class unsafeClass = Class.forName("sun.misc.Unsafe"); + java.lang.reflect.Field unsafeField = unsafeClass.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + Object unsafe = unsafeField.get(null); + Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class); + Object activityThread = allocateInstance.invoke(unsafe, activityThreadClass); + Method getSystemContext = activityThreadClass.getDeclaredMethod("getSystemContext"); + getSystemContext.setAccessible(true); + return (Context) getSystemContext.invoke(activityThread); + } catch (Exception e) { + Log.e(TAG, "failed to create Android system context", e); + return null; + } + } + + private static final class BridgeBinder extends ICompatibleBridge.Stub { + private final String socketPath; + private LocalSocket socket; + + BridgeBinder(String socketPath) { + this.socketPath = socketPath; + } + + @Override + public synchronized ParcelFileDescriptor getConnection() { + try { + if (socket == null) { + LocalSocket candidate = new LocalSocket(); + try { + candidate.connect(new LocalSocketAddress(socketPath, + LocalSocketAddress.Namespace.FILESYSTEM)); + socket = candidate; + } catch (IOException e) { + candidate.close(); + Log.e(TAG, "failed to connect to daemon socket " + socketPath, e); + return null; + } + } + return ParcelFileDescriptor.dup(socket.getFileDescriptor()); + } catch (IOException e) { + closeSocket(); + Log.e(TAG, "failed to duplicate daemon socket fd", e); + return null; + } + } + + private void closeSocket() { + if (socket == null) + return; + try { + socket.close(); + } catch (IOException ignored) { + } + socket = null; + } + } +} diff --git a/app/src/main/java/com/anland/termux/MainActivity.java b/app/src/main/java/com/anland/termux/MainActivity.java index 50b6e76..1cf0628 100644 --- a/app/src/main/java/com/anland/termux/MainActivity.java +++ b/app/src/main/java/com/anland/termux/MainActivity.java @@ -8,7 +8,10 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.app.PictureInPictureParams; +import android.content.BroadcastReceiver; +import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; @@ -18,6 +21,9 @@ import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; import android.util.Log; import android.util.SparseArray; import android.view.Display; @@ -68,6 +74,9 @@ public class MainActivity extends Activity // Camera service fds/threads are created once and persist across reconnects; // this guards that one-time init (see applyCameraState). private boolean cameraInited = false; + private ICompatibleBridge compatibleBridge; + private boolean compatibleFdReady = false; + private boolean compatibleReceiverRegistered = false; private static final String DEFAULT_SOCKET_PATH = "/data/data/com.termux/files/usr/tmp/anland/display_daemon.sock"; private static final String KEY_ACCESSIBILITY_ENABLED = "accessibility_key_intercept"; private static final String KEY_EXTRA_KEYS_ENABLED = "extra_keys_bar"; @@ -125,6 +134,43 @@ public class MainActivity extends Activity // Layout JSON the current bar was built from; used to detect edits on resume. private String mAppliedLayoutJson = ""; + private final BroadcastReceiver compatibleBridgeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!CompatibleBridge.ACTION_START.equals(intent.getAction())) + return; + + Bundle bundle = intent.getBundleExtra(null); + IBinder binder = bundle == null ? null : bundle.getBinder(null); + if (binder == null) + return; + + ICompatibleBridge bridge = ICompatibleBridge.Stub.asInterface(binder); + boolean newBridge = compatibleBridge == null + || !compatibleBridge.asBinder().isBinderAlive(); + if (newBridge) { + compatibleBridge = bridge; + compatibleFdReady = false; + try { + binder.linkToDeath(() -> runOnUiThread(() -> { + if (compatibleBridge != null + && compatibleBridge.asBinder() == binder) { + compatibleBridge = null; + compatibleFdReady = false; + } + }), 0); + } catch (RemoteException ignored) { + } + } + + // Wait until the Surface exists before asking the bridge to connect to + // the daemon. The daemon reads the consumer hello synchronously, so a + // connection opened before nativeStart() would temporarily block it. + if (surfaceReady) + attachCompatibleFd(); + } + }; + public static MainActivity sInstance; // ADDED: VirtualKeyboardView instance @@ -172,9 +218,8 @@ private void pushRefreshRate() { } // Push the current connection settings (socket path / root mode) to native - // before (re)connecting. The root helper is the executable bundled in the - // app's native lib dir; the bridge is a unix socket in our cache dir that - // the helper, launched via su, uses to hand back the daemon fd. + // before (re)connecting. Compatible builds receive the daemon fd from the + // Termux-side Binder bridge instead of calling connect() from this UID. private void applyConnectionConfig() { SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); String sock = prefs.getString(KEY_SOCKET_PATH, DEFAULT_SOCKET_PATH); @@ -183,7 +228,10 @@ private void applyConnectionConfig() { boolean useRoot = prefs.getBoolean(KEY_USE_ROOT, false); String helperPath = getApplicationInfo().nativeLibraryDir + "/libfdhelper.so"; String bridgePath = getCacheDir().getAbsolutePath() + "/anland_fdbridge.sock"; + Native.nativeSetCompatibleMode(BuildConfig.COMPATIBLE); Native.nativeConfigure(sock.trim(), useRoot, helperPath, bridgePath); + if (BuildConfig.COMPATIBLE) + attachCompatibleFd(); int customW = prefs.getInt("custom_width", 0); int customH = prefs.getInt("custom_height", 0); customScreenWidth = prefs.getInt("custom_width", 0); @@ -191,6 +239,48 @@ private void applyConnectionConfig() { Native.nativeSetCustomResolution(customW, customH); } + private void registerCompatibleReceiver() { + if (!BuildConfig.COMPATIBLE || compatibleReceiverRegistered) + return; + + IntentFilter filter = new IntentFilter(CompatibleBridge.ACTION_START); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + registerReceiver(compatibleBridgeReceiver, filter, Context.RECEIVER_EXPORTED); + else + registerReceiver(compatibleBridgeReceiver, filter); + compatibleReceiverRegistered = true; + } + + private void attachCompatibleFd() { + if (!BuildConfig.COMPATIBLE || compatibleBridge == null || compatibleFdReady) + return; + + ParcelFileDescriptor pfd = null; + int fd = -1; + try { + pfd = compatibleBridge.getConnection(); + if (pfd == null) + return; + fd = pfd.detachFd(); + Native.nativeSetCompatibleFd(fd); + compatibleFdReady = true; + } catch (Exception e) { + Log.e(TAG, "failed to receive compatible daemon socket fd", e); + } finally { + if (fd < 0 && pfd != null) { + try { + pfd.close(); + } catch (Exception ignored) { + } + } + } + } + + private void stopNative() { + Native.nativeStop(); + compatibleFdReady = false; + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -199,6 +289,7 @@ protected void onCreate(Bundle savedInstanceState) { sInstance = this; clipboard = new Clipboard(this); + registerCompatibleReceiver(); mDisplayCutoutMode = DisplayCutoutMode.get( getSharedPreferences(PREFS_NAME, MODE_PRIVATE)); @@ -577,7 +668,7 @@ && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) // later reconnect. Idempotent, so safe to call on every resume. applyCameraState(); if (surfaceReady) { - Native.nativeStop(); + stopNative(); applyConnectionConfig(); Native.nativeStart(surfaceView.getHolder().getSurface(), clipboard); pushRefreshRate(); @@ -695,7 +786,7 @@ protected void onPause() { if (dm != null) dm.unregisterDisplayListener(displayListener); if (!mPipTransitionPending && !isInPictureInPictureMode()) - Native.nativeStop(); + stopNative(); } private boolean hasPipPermission() { @@ -742,7 +833,11 @@ public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, @Override protected void onDestroy() { - Native.nativeStop(); + stopNative(); + if (compatibleReceiverRegistered) { + unregisterReceiver(compatibleBridgeReceiver); + compatibleReceiverRegistered = false; + } NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); if (nm != null) nm.cancel(NOTIFICATION_ID); if (cameraInited) { @@ -843,7 +938,7 @@ public void surfaceChanged(SurfaceHolder holder, int format, int width, int heig surfaceReady = true; // Same ordering guarantee as onResume: camera service settled before connect. applyCameraState(); - Native.nativeStop(); + stopNative(); applyConnectionConfig(); Native.nativeStart(holder.getSurface(), clipboard); pushRefreshRate(); @@ -859,7 +954,7 @@ public void surfaceDestroyed(SurfaceHolder holder) { surfaceReady = false; releaseAllMouseButtons(); resetCapturedTouchpadGesture(); - Native.nativeStop(); + stopNative(); } diff --git a/app/src/main/java/com/anland/termux/Native.java b/app/src/main/java/com/anland/termux/Native.java index 5f0e24c..6afb7e2 100644 --- a/app/src/main/java/com/anland/termux/Native.java +++ b/app/src/main/java/com/anland/termux/Native.java @@ -15,6 +15,8 @@ private Native() {} public static native void nativeConfigure(String socketPath, boolean useRoot, String helperPath, String bridgePath); + public static native void nativeSetCompatibleMode(boolean enabled); + public static native void nativeSetCompatibleFd(int fd); // With static natives there is no `thiz`, so native is handed the object it // calls back into (the Clipboard instance hosting nativeSetClipboardText / diff --git a/app/src/main/jni/native_consumer.c b/app/src/main/jni/native_consumer.c index e17b87c..f556d7c 100644 --- a/app/src/main/jni/native_consumer.c +++ b/app/src/main/jni/native_consumer.c @@ -84,6 +84,8 @@ static struct consumer_state g_state = { static pthread_mutex_t cfg_lock = PTHREAD_MUTEX_INITIALIZER; static char cfg_socket_path[256] = "/data/data/com.termux/files/usr/tmp/anland/display_daemon.sock"; static bool cfg_use_root = false; +static bool cfg_use_compatible_bridge = false; +static int cfg_compatible_fd = -1; static char cfg_helper_path[512] = ""; static char cfg_bridge_path[512] = ""; @@ -383,6 +385,8 @@ static int do_connect(struct consumer_state *s) /* Snapshot the connection config for this attempt. */ pthread_mutex_lock(&cfg_lock); bool use_root = cfg_use_root; + bool use_compatible_bridge = cfg_use_compatible_bridge; + bool compatible_fd_available = cfg_compatible_fd >= 0; char sock_path[sizeof(cfg_socket_path)]; char helper_path[sizeof(cfg_helper_path)]; char bridge_path[sizeof(cfg_bridge_path)]; @@ -391,6 +395,11 @@ static int do_connect(struct consumer_state *s) memcpy(bridge_path, cfg_bridge_path, sizeof(bridge_path)); pthread_mutex_unlock(&cfg_lock); + if (use_compatible_bridge && !compatible_fd_available) { + LOGI("compatible bridge has not supplied a socket fd yet"); + return -1; + } + const char *sock = sock_path; if (s->ctx) { @@ -439,10 +448,26 @@ static int do_connect(struct consumer_state *s) if (collect_dmabufs(s) < 0) return -1; - LOGI("connecting to %s (%dx%d, %d bufs, root=%d)", sock, - s->screen_w, s->screen_h, s->buf_count, use_root); - - if (use_root) { + LOGI("connecting to %s (%dx%d, %d bufs, root=%d, compatible=%d)", sock, + s->screen_w, s->screen_h, s->buf_count, use_root, + use_compatible_bridge); + + if (use_compatible_bridge) { + pthread_mutex_lock(&cfg_lock); + /* Keep the Binder-supplied descriptor as a master copy. Each display + * context owns a duplicate, while the Termux bridge keeps the actual + * daemon connection alive across consumer reconnect attempts. */ + int compatible_fd = cfg_compatible_fd >= 0 ? dup(cfg_compatible_fd) : -1; + pthread_mutex_unlock(&cfg_lock); + if (compatible_fd < 0) { + LOGE("could not duplicate compatible bridge socket fd: %s", strerror(errno)); + return -1; + } + if (connect_to_deamon_with_fd(&s->ctx, compatible_fd) < 0) { + LOGE("connect_to_deamon_with_fd from compatible bridge failed"); + return -1; + } + } else if (use_root) { int ctrl_fd = recv_fd_via_root_helper(sock, helper_path, bridge_path); if (ctrl_fd < 0) { LOGE("root helper connect failed"); @@ -638,6 +663,43 @@ Java_com_anland_termux_Native_nativeConfigure( cfg_socket_path, cfg_use_root, cfg_helper_path, cfg_bridge_path); } +JNIEXPORT void JNICALL +Java_com_anland_termux_Native_nativeSetCompatibleMode( + JNIEnv *env, jclass clazz, jboolean enabled) +{ + (void)env; + (void)clazz; + + pthread_mutex_lock(&cfg_lock); + cfg_use_compatible_bridge = (enabled == JNI_TRUE); + if (!cfg_use_compatible_bridge && cfg_compatible_fd >= 0) { + close(cfg_compatible_fd); + cfg_compatible_fd = -1; + } + pthread_mutex_unlock(&cfg_lock); + + LOGI("compatible bridge mode: %d", enabled == JNI_TRUE); +} + +JNIEXPORT void JNICALL +Java_com_anland_termux_Native_nativeSetCompatibleFd( + JNIEnv *env, jclass clazz, jint fd) +{ + (void)env; + (void)clazz; + + if (fd < 0) + return; + + pthread_mutex_lock(&cfg_lock); + if (cfg_compatible_fd >= 0) + close(cfg_compatible_fd); + cfg_compatible_fd = fd; + pthread_mutex_unlock(&cfg_lock); + + LOGI("received compatible bridge socket fd=%d", fd); +} + JNIEXPORT void JNICALL Java_com_anland_termux_Native_nativeSetCustomResolution( JNIEnv* env, jclass clazz, jint width, jint height) @@ -757,6 +819,13 @@ Java_com_anland_termux_Native_nativeStop( cleanup_dmabufs(&g_state); + pthread_mutex_lock(&cfg_lock); + if (cfg_compatible_fd >= 0) { + close(cfg_compatible_fd); + cfg_compatible_fd = -1; + } + pthread_mutex_unlock(&cfg_lock); + if (g_state.window) { ANativeWindow_release(g_state.window); g_state.window = NULL; diff --git a/app/src/standard/AndroidManifest.xml b/app/src/standard/AndroidManifest.xml new file mode 100644 index 0000000..26ed7ec --- /dev/null +++ b/app/src/standard/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 3f35c52..13876a5 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -12,8 +12,10 @@ Based on Anland, this project connects the Android display client, the Termux da - The Java layer handles interactions including activities, settings, input, clipboard, camera, and audio. - `app/src/main/jni/` contains native code for Surface, dma-buf, Unix sockets, and the JNI bridge. - Its package name is `com.anland.termux` and its app name is `Anland Termux`. - - It uses the shared UID `com.termux` and is signed with `app/testkey_untrusted.jks` for compatibility with the GitHub version of Termux. + - The `standard` flavor uses the shared UID `com.termux` and is signed with `app/testkey_untrusted.jks` for the GitHub version of Termux. + - The `compatible` flavor omits `sharedUserId`, keeps the same application ID, versionCode, and signing key, and appends `-compatible` to versionName. - `termux/anland/`: the `anland` daemon on the Termux side, which relays control messages and file descriptors between the Android display client and the Wayland producer. Its default socket is `$TMPDIR/anland/display_daemon.sock`; if `TMPDIR` is unset, it falls back to `/data/data/com.termux/files/usr/tmp/anland/display_daemon.sock`. +- `termux/anland/anland-compatible`: Termux-side launcher for the compatible APK. It resolves the installed APK and starts `CompatibleBridge` with `app_process`. - `packages/anland/`: draft Termux Packages recipe for building the daemon as a Termux package. - `scripts/`: Helper startup scripts for KDE Plasma and Weston in the Termux native environment and PRoot, Chroot, and LXC containers. - `images/`: ARM64 PRoot container image definitions for Debian 13 and Ubuntu 26.04. `images/packages.json` records download URLs for KWin, Weston, XWayland, and Mesa build artifacts. @@ -22,6 +24,43 @@ Based on Anland, this project connects the Android display client, the Termux da - `docs/`: English and Chinese user and developer documentation; Chinese files use the `_zh.md` suffix. - `out/`: local build output directory for APKs and the daemon; do not commit it to Git. +## Transports + +### Standard Transport + +The standard APK relies on Android's shared-UID mechanism and is intended for the GitHub release of Termux: + +1. The `standard` flavor manifest declares `android:sharedUserId="com.termux"` and is signed with `app/testkey_untrusted.jks`. During installation, Android verifies that its signature matches the installed Termux package; when it does, both apps run under the same Linux UID. +2. The `anland` daemon listens on `display_daemon.sock` as the Termux UID. Because the standard APK runs with that UID too, its display client can access the Unix socket without cross-UID socket permissions or an extra bridge process. +3. After `MainActivity` configures the default or user-selected socket path, the native consumer calls `connect_to_deamon()` outside compatible mode. That function calls `connect_unix()` to connect directly to the daemon. This path neither starts `anland-compatible` nor uses Binder. +4. Once the control connection is established, the native display context sends the consumer hello and its display-side file descriptors. The daemon registers it as the consumer, then relays screen information and required file descriptors when the Wayland producer connects. + +This transport depends on the Termux and standard APK signatures matching. F-Droid Termux and variants signed with a different key cannot meet that condition, so Android rejects installation or updates of the standard APK; use the compatible APK in those environments. With the standard APK, start only the `anland` daemon, not `anland-compatible`. + +### Compatible Transport + +The compatible APK follows the standalone Termux:X11 transport and does not need +the F-Droid signing key: + +1. `anland-compatible` runs under the Termux UID and loads + `com.anland.termux.CompatibleBridge` from the installed APK with + `/system/bin/app_process`. +2. `CompatibleBridge` connects to `display_daemon.sock` as the Termux UID, + keeps the connected `LocalSocket` open, and sends a package-targeted + broadcast containing an `ICompatibleBridge` Binder. +3. `MainActivity` receives the Binder, calls `getConnection()`, and detaches the + returned `ParcelFileDescriptor`. `Native.nativeSetCompatibleFd()` gives the + duplicate fd to `connect_to_deamon_with_fd()`. +4. The native display context owns that fd for its lifetime. Its existing + fallback protocol can redeposit fresh data/fence/audio fds over the same + control connection, so no Android-side Unix `connect()` is needed. + +The user starts `anland-compatible [SOCKET_PATH]` from Termux, just as the +standalone Termux:X11 command starts its entry point. The bridge keeps running +and republishes its package-targeted Binder so an Activity recreated later can +obtain another duplicate fd; no F-Droid signing key or Termux plugin permission +is involved. + ## Debugging Set debugging variables before running the startup script. The examples below work in both the Termux native environment and containers. PRoot-Distro must be entered with `--shared-tmp`; Chroot and LXC containers must share the directory containing the Anland socket with Termux. @@ -150,12 +189,22 @@ Build script: tools/build-app.sh ``` -Build artifact: +This builds the `standardDebug` flavor. The compatible flavor is built with: + +```sh +tools/build-compatible-app.sh +``` + +Build artifacts: ```text out/AnlandTermux-.apk +out/AnlandTermux--compatible.apk ``` +Both flavors use `app/testkey_untrusted.jks` and the same `versionCode`. The +compatible flavor gets its `-compatible` versionName suffix from Gradle. + ### Anland Daemon Build script: @@ -170,6 +219,9 @@ When this is run inside Termux, the output is a Termux executable: out/anland ``` +`make -C termux/anland install` and the package recipe also install the +`anland-compatible` launcher required by the compatible APK. + The draft Termux package recipe is located at: ```text @@ -270,7 +322,7 @@ Manual input: - `ref`: required Git reference to build; may be a branch, TAG, or commit. The default is `termux`. -The build environment is fixed to JDK 21, Gradle 9.6.0, and Android NDK 29.0.14206865, and invokes `tools/build-app.sh`. +The build environment is fixed to JDK 21, Gradle 9.6.0, and Android NDK 29.0.14206865, and invokes both `tools/build-app.sh` and `tools/build-compatible-app.sh`. Pull-request builds add `-debug-` before the compatible suffix, for example `AnlandTermux-5.13.2-debug-70d1b85-compatible.apk`. ### Build Docker Images diff --git a/docs/developer-guide_zh.md b/docs/developer-guide_zh.md index dd28438..edc7665 100644 --- a/docs/developer-guide_zh.md +++ b/docs/developer-guide_zh.md @@ -12,8 +12,10 @@ - Java 层负责 Activity、设置、输入、剪贴板、相机和音频等交互。 - `app/src/main/jni/` 包含 Surface、dma-buf、Unix Socket 及 JNI 桥接等原生代码。 - 包名为 `com.anland.termux`,应用名称为 `Anland Termux`。 - - 使用共享 UID `com.termux`,并通过 `app/testkey_untrusted.jks` 与 Termux GitHub 版本兼容签名。 + - `standard` flavor 使用共享 UID `com.termux`,并通过 `app/testkey_untrusted.jks` 与 Termux GitHub 版本兼容签名。 + - `compatible` flavor 不使用 `sharedUserId`,但保持相同的 application ID、versionCode 和签名密钥,并在 versionName 后添加 `-compatible`。 - `termux/anland/`:Termux 侧的 `anland` 守护程序,在 Android 显示端与 Wayland 生产端之间中继控制消息和文件描述符。默认套接字为 `$TMPDIR/anland/display_daemon.sock`;`TMPDIR` 未设置时回退到 `/data/data/com.termux/files/usr/tmp/anland/display_daemon.sock`。 +- `termux/anland/anland-compatible`:compatible APK 的 Termux 侧启动脚本,通过 `app_process` 从已安装的 APK 启动 `CompatibleBridge`。 - `packages/anland/`:Termux Packages 配方草稿,用于将守护程序构建为 Termux 软件包。 - `scripts/`:Termux 原生环境及 PRoot、Chroot、LXC 容器中的 KDE Plasma 和 Weston 一键启动脚本。 - `images/`:Debian 13 和 Ubuntu 26.04 的 ARM64 PRoot 容器镜像定义;`images/packages.json` 记录 KWin、Weston、XWayland 和 Mesa 构建产物的下载地址。 @@ -22,6 +24,30 @@ - `docs/`:中英文用户文档和开发者文档;中文文件使用 `_zh.md` 后缀。 - `out/`:本地构建脚本生成的 APK 和守护程序输出目录,不应提交到 Git。 +## 传输方式 + +### Standard 传输方式 + +Standard APK 依赖 Android 的 shared UID 机制,适用于 GitHub 发布的 Termux: + +1. `standard` flavor 的 manifest 声明 `android:sharedUserId="com.termux"`,并使用 `app/testkey_untrusted.jks` 签名。安装时,Android 会校验它与已安装 Termux 的签名是否匹配;匹配后,两个应用使用同一个 Linux UID。 +2. `anland` 守护程序以 Termux UID 在 `display_daemon.sock` 上监听。由于 Standard APK 也以该 UID 运行,显示端可访问这个 Unix socket,不需要跨 UID 的 socket 权限,也不需要额外的 bridge 进程。 +3. `MainActivity` 配置默认或用户设置的 socket 路径后,native consumer 在非 compatible 模式下调用 `connect_to_deamon()`;该函数通过 `connect_unix()` 直接连接守护程序。此路径不启动 `anland-compatible`,也不经过 Binder。 +4. 建立控制连接后,native display context 发送 consumer hello 及其显示侧文件描述符。守护程序将该连接登记为 consumer,并在 Wayland producer 连接后中继 screen 信息和所需的文件描述符。 + +这种方式依赖 Termux 与 Standard APK 的签名匹配。F-Droid 版 Termux 及其他使用不同签名密钥的变体无法满足该条件,Android 会拒绝安装或更新 Standard APK;这些环境应使用 Compatible APK。使用 Standard APK 时只需启动 `anland` 守护程序,不应启动 `anland-compatible`。 + +### Compatible 传输方式 + +Compatible APK 参照 Termux:X11 的独立版传输方式,不需要 F-Droid 的签名密钥: + +1. `anland-compatible` 以 Termux UID 运行,通过 `/system/bin/app_process` 从已安装的 APK 加载 `com.anland.termux.CompatibleBridge`。 +2. `CompatibleBridge` 以 Termux UID 连接 `display_daemon.sock`,持有连接的 `LocalSocket`,并向指定包发送包含 `ICompatibleBridge` Binder 的广播。 +3. `MainActivity` 接收 Binder,调用 `getConnection()`,并 detach 返回的 `ParcelFileDescriptor`;`Native.nativeSetCompatibleFd()` 将重复的 fd 交给 `connect_to_deamon_with_fd()`。 +4. native display context 在整个生命周期内持有该 fd。现有 fallback 协议可以在同一控制连接上重新传递 data/fence/audio fd,因此 Android 侧不需要调用 Unix `connect()`。 + +用户在 Termux 中运行 `anland-compatible [SOCKET_PATH]` 启动 bridge,方式与独立版 Termux:X11 启动其入口点相同。bridge 会持续运行并重复发布指定包的 Binder,使 Activity 重建后仍能获取新的重复 fd;不需要 F-Droid 签名密钥,也不需要 Termux 插件权限。 + ## 调试 调试变量应在启动脚本前设置。下列示例既可用于 Termux 原生环境,也可在容器中使用;PRoot-Distro 必须通过 `--shared-tmp` 进入,Chroot 或 LXC 则应确保容器与 Termux 共享 Anland Socket 所在目录。 @@ -150,12 +176,21 @@ compileSdk 36 tools/build-app.sh ``` +该脚本构建 `standardDebug` flavor。compatible flavor 使用: + +```sh +tools/build-compatible-app.sh +``` + 构建产物: ```text out/AnlandTermux-.apk +out/AnlandTermux--compatible.apk ``` +两个 flavor 都使用 `app/testkey_untrusted.jks`,并保持相同的 `versionCode`;compatible flavor 的 `-compatible` versionName 后缀由 Gradle 添加。 + ### Anland 守护程序 构建脚本: @@ -170,6 +205,8 @@ tools/build-termux-anland.sh out/anland ``` +`make -C termux/anland install` 和软件包配方还会安装 compatible APK 所需的 `anland-compatible` 启动脚本。 + Termux 软件包的配方草稿位于: ```text @@ -263,14 +300,14 @@ Termux 软件包关联的 Pull requests:https://github.com/termux/termux-packa 触发方式: -- Pull request:当目标分支为 `termux` 且 `app/**` 发生变化时自动运行,构建 GitHub 提供的 PR 合并引用。 +- Pull request:当目标分支为 `termux` 且 APK、Termux bridge、构建脚本或工作流发生变化时自动运行,构建 GitHub 提供的 PR 合并引用。 - 手动触发:通过 `workflow_dispatch` 运行。 手动输入: - `ref`:必填,要构建的 Git 引用,可以是分支、TAG 或 commit;默认值为 `termux`。 -构建环境固定使用 JDK 21、Gradle 9.6.0 和 Android NDK 29.0.14206865,并调用 `tools/build-app.sh`。 +构建环境固定使用 JDK 21、Gradle 9.6.0 和 Android NDK 29.0.14206865,并同时调用 `tools/build-app.sh` 与 `tools/build-compatible-app.sh`。Pull request 构建会在 compatible 后缀之前加入 `-debug-<短 SHA>`,例如 `AnlandTermux-5.13.2-debug-70d1b85-compatible.apk`。 ### Build Docker Images diff --git a/docs/user-guide.md b/docs/user-guide.md index bfa7966..d2d46c9 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -8,10 +8,17 @@ This guide walks you through downloading, installing, and using [Anland: Termux] ## Prerequisites -Before installing Anland: Termux, check that your installed Termux app comes from the [official GitHub releases](https://github.com/termux/termux-app/releases) **(not F-Droid or Google Play)**. This project supports only the Termux app from the official GitHub releases. To migrate Termux to the GitHub version, refer to the official backup and restore guide: +Anland: Termux provides two display APKs. Choose the one that matches the installed Termux app: + +| Termux source | Display APK | Transport | +| --- | --- | --- | +| [Official GitHub releases](https://github.com/termux/termux-app/releases) | `AnlandTermux-.apk` | Shared UID and direct Unix socket connection | +| [F-Droid](https://f-droid.org/packages/com.termux/) or variants such as ZeroTermux | `AnlandTermux--compatible.apk` | Termux-side socket plus Binder fd transfer | + +The two APKs use the same application ID and versionCode, so they cannot be installed side by side. Uninstall the previous Anland Termux APK before switching transport variants. ```sh -# Run this command in Termux; it should output 'GITHUB' +# Run this command in Termux. Use the compatible APK when it reports F_DROID. echo $TERMUX_APP__APK_RELEASE ``` @@ -21,8 +28,9 @@ In the [latest release notes](https://github.com/lfdevs/anland-termux/releases/l | Item | Filename | | :---: | --- | -| Android Display App | `AnlandTermux-5.13.1.apk` | -| Termux Daemon | `anland_5.11.0-1_aarch64.deb` | +| Android Display App (Standard) | `AnlandTermux-5.13.3.apk` | +| Android Display App (Compatible) | `AnlandTermux-5.13.3-compatible.apk` | +| Termux Daemon | `anland_5.13.3_aarch64.deb` | | | XWayland | KWin | Weston | | :---: | --- | --- | --- | @@ -30,22 +38,20 @@ In the [latest release notes](https://github.com/lfdevs/anland-termux/releases/l | Ubuntu 26.04 | `xwayland_24.1.10-91_arm64.deb` | `kwin_anland-5.8-4_6.6.4-0ubuntu92.zip` | `weston_anland-5.13-ubuntu-14.0.2-92.zip` | | Debian 13 | `xwayland_24.1.6-91_arm64.deb` | `kwin_anland-5.8-debian-4_6.3.6-92.zip` | `weston_anland-5.13-debian-14.0.2-92.zip` | -The Android Display App and Termux Daemon are required. Choose the XWayland, Weston, and KWin versions that match your runtime environment. +The Android Display App and Termux Daemon are required. Choose the display APK according to the table above, then choose the XWayland, Weston, and KWin versions that match your runtime environment. -For example, to run Anland: Termux with KDE Plasma in a Debian 13 PRoot container, download these four files: `AnlandTermux-5.13.1.apk`, `anland_5.11.0-1_aarch64.deb`, `xwayland_24.1.6-91_arm64.deb`, and `kwin_anland-5.8-debian-4_6.3.6-92.zip`. +For example, to run Anland: Termux with KDE Plasma in a Debian 13 PRoot container using F-Droid Termux, download these four files: `AnlandTermux-5.13.3-compatible.apk`, `anland_5.13.3_aarch64.deb`, `xwayland_24.1.6-91_arm64.deb`, and `kwin_anland-5.8-debian-4_6.3.6-92.zip`. -Or, to run Anland: Termux with Weston in an Ubuntu 26.04 Chroot container, download these four files: `AnlandTermux-5.13.1.apk`, `anland_5.11.0-1_aarch64.deb`, `xwayland_24.1.10-91_arm64.deb`, and `weston_anland-5.13-ubuntu-14.0.2-92.zip`. +Or, to run Anland: Termux with Weston in an Ubuntu 26.04 Chroot container using the GitHub Termux release, download these four files: `AnlandTermux-5.13.3.apk`, `anland_5.13.3_aarch64.deb`, `xwayland_24.1.10-91_arm64.deb`, and `weston_anland-5.13-ubuntu-14.0.2-92.zip`. ## Installation -1. Install the display app on Android, such as `AnlandTermux-5.13.0.apk`. +1. Install the display app on Android: use `AnlandTermux-5.13.3.apk` with GitHub Termux, or `AnlandTermux-5.13.3-compatible.apk` with F-Droid Termux. After installation, **long-press the app icon** to open its settings interface. - After installation, **long-press the app icon** to open its settings interface. - -2. Install the daemon in Termux, such as `anland_5.11.0-1_aarch64.deb`. +2. Install the daemon in Termux, such as `anland_5.13.3_aarch64.deb`. ```sh - pkg reinstall ./anland_5.11.0-1_aarch64.deb + pkg reinstall ./anland_5.13.3_aarch64.deb ``` > [!TIP] @@ -66,8 +72,10 @@ Or, to run Anland: Termux with Weston in an Ubuntu 26.04 Chroot container, downl > To use it, run the following commands. This starts KDE Plasma or Weston. Then switch to the “Anland Termux” app on Android. The `ANLAND_WESTON_SCALE` environment variable in the commands sets Weston’s scaling factor; set it to an integer that suits your needs. > > ```sh -> killall anland > /dev/null 2>&1 -> anland > /dev/null 2>&1 & +> # Start the daemon: +> killall anland > /dev/null 2>&1; anland > /dev/null 2>&1 & +> # The compatible APK also requires the Binder bridge: +> pkill -TERM -x anland-compatible; anland-compatible & > # Debian 13 with KDE Plasma: > proot-distro login debian-anland --shared-tmp -- bash -c "startplasma-anland" > # Ubuntu 26.04 with KDE Plasma: @@ -91,7 +99,7 @@ Or, to run Anland: Termux with Weston in an Ubuntu 26.04 Chroot container, downl ```sh sudo apt reinstall ./xwayland_24.1.6-91_arm64.deb unzip kwin_anland-5.8-debian-4_6.3.6-92.zip -d kwin-debs-install/ - sudo apt reinstall kwin-debs-install/*.deb + sudo apt reinstall ./kwin-debs-install/*.deb rm -rf kwin-debs-install/ ``` @@ -100,7 +108,7 @@ Or, to run Anland: Termux with Weston in an Ubuntu 26.04 Chroot container, downl ```sh sudo apt reinstall ./xwayland_24.1.10-91_arm64.deb unzip weston_anland-5.13-ubuntu-14.0.2-92.zip -d weston-debs-install/ - sudo apt reinstall weston-debs-install/*.deb + sudo apt reinstall ./weston-debs-install/*.deb rm -rf weston-debs-install/ ``` @@ -153,10 +161,13 @@ Or, to run Anland: Termux with Weston in an Ubuntu 26.04 Chroot container, downl 1. Start the daemon in Termux: ```sh - killall anland > /dev/null 2>&1 - anland > /dev/null 2>&1 & + killall anland > /dev/null 2>&1; anland > /dev/null 2>&1 & + # The compatible APK also requires the Binder bridge: + pkill -TERM -x anland-compatible; anland-compatible & ``` + For the compatible APK, keep `anland-compatible` running in Termux while the display app and daemon are in use. + 2. If your runtime environment is a Linux container, bind-mount Termux’s `$TMPDIR` to `/tmp` inside the container. For example, add the `--shared-tmp` option when logging into a PRoot-Distro container: @@ -168,7 +179,7 @@ Or, to run Anland: Termux with Weston in an Ubuntu 26.04 Chroot container, downl 3. After entering the runtime environment, download and run the following helper scripts. > [!TIP] -> To ensure that audio services work correctly, make sure PipeWire is installed before running either script. For example, install the `pipewire` package in Termux or the `pipewire-audio` package in Debian/Ubuntu. +> To ensure that audio services work correctly, make sure PipeWire is installed before running either script. For example, install the `pipewire` package in Termux or the `pipewire-audio` and `pipewire-libcamera` packages in Debian/Ubuntu. KDE Plasma: [startplasma-anland.sh](../scripts/startplasma-anland.sh) diff --git a/docs/user-guide_zh.md b/docs/user-guide_zh.md index 8b916d8..883efed 100644 --- a/docs/user-guide_zh.md +++ b/docs/user-guide_zh.md @@ -8,10 +8,17 @@ ## 前提 -在安装 Anland: Termux 前,请先检查你现在安装的 Termux App 是否来自 [GitHub 的官方 Releases](https://github.com/termux/termux-app/releases)**(而非 F-Droid 或 Google Play)**。本项目仅支持与来自 GitHub 官方 Releases 的 Termux App 一同工作。如果你想将 Termux 迁移到 GitHub 版本,可以参考官方的备份与恢复指南: +Anland: Termux 提供两种显示 APK,请按已安装的 Termux App 来源选择: + +| Termux 来源 | 显示 APK | 传输方式 | +| --- | --- | --- | +| [GitHub 官方 Releases](https://github.com/termux/termux-app/releases) | `AnlandTermux-.apk` | Shared UID 和直接 Unix socket 连接 | +| [F-Droid](https://f-droid.org/packages/com.termux/) 或 ZeroTermux 等变体 | `AnlandTermux--compatible.apk` | Termux 侧 socket 和 Binder fd 传递 | + +两种 APK 使用相同的 application ID 和 versionCode,不能同时安装。切换传输版本前请先卸载旧的 Anland Termux APK。 ```sh -# 在 Termux 运行这条命令,输出应为 'GITHUB' +# 在 Termux 运行这条命令;输出为 F_DROID 时请选择 compatible APK。 echo $TERMUX_APP__APK_RELEASE ``` @@ -21,8 +28,9 @@ echo $TERMUX_APP__APK_RELEASE | Item | Filename | | :---: | --- | -| Android Display App | `AnlandTermux-5.13.1.apk` | -| Termux Daemon | `anland_5.11.0-1_aarch64.deb` | +| Android Display App (Standard) | `AnlandTermux-5.13.3.apk` | +| Android Display App (Compatible) | `AnlandTermux-5.13.3-compatible.apk` | +| Termux Daemon | `anland_5.13.3_aarch64.deb` | | | XWayland | KWin | Weston | | :---: | --- | --- | --- | @@ -30,22 +38,20 @@ echo $TERMUX_APP__APK_RELEASE | Ubuntu 26.04 | `xwayland_24.1.10-91_arm64.deb` | `kwin_anland-5.8-4_6.6.4-0ubuntu92.zip` | `weston_anland-5.13-ubuntu-14.0.2-92.zip` | | Debian 13 | `xwayland_24.1.6-91_arm64.deb` | `kwin_anland-5.8-debian-4_6.3.6-92.zip` | `weston_anland-5.13-debian-14.0.2-92.zip` | -“Android Display App”和“Termux Daemon”是必需的。“XWayland”、“Weston”和“KWin”和的版本请根据你的实际运行环境来进行选择。 +“Android Display App”和“Termux Daemon”是必需的。请先按上表选择显示 APK,再根据实际运行环境选择“XWayland”、“Weston”和“KWin”的版本。 -例如,在 Debian 13 的 PRoot 容器中运行 Anland: Termux,并使用 KDE Plasma,需要下载 `AnlandTermux-5.13.1.apk`、`anland_5.11.0-1_aarch64.deb`、`xwayland_24.1.6-91_arm64.deb` 和 `kwin_anland-5.8-debian-4_6.3.6-92.zip` 四个文件。 +例如,在 Debian 13 的 PRoot 容器中使用 F-Droid Termux 运行 Anland: Termux 和 KDE Plasma,需要下载 `AnlandTermux-5.13.3-compatible.apk`、`anland_5.13.3_aarch64.deb`、`xwayland_24.1.6-91_arm64.deb` 和 `kwin_anland-5.8-debian-4_6.3.6-92.zip` 四个文件。 -又如,在 Ubuntu 26.04 的 Chroot 容器中运行 Anland: Termux,并使用 Weston,需要下载 `AnlandTermux-5.13.1.apk`、`anland_5.11.0-1_aarch64.deb`、`xwayland_24.1.10-91_arm64.deb` 和 `weston_anland-5.13-ubuntu-14.0.2-92.zip` 四个文件。 +又如,在 Ubuntu 26.04 的 Chroot 容器中使用 GitHub Termux 运行 Anland: Termux 和 Weston,需要下载 `AnlandTermux-5.13.3.apk`、`anland_5.13.3_aarch64.deb`、`xwayland_24.1.10-91_arm64.deb` 和 `weston_anland-5.13-ubuntu-14.0.2-92.zip` 四个文件。 ## 安装 -1. 在 Android 安装显示应用,如 `AnlandTermux-5.13.0.apk`。 +1. 在 Android 安装显示应用:GitHub Termux 使用 `AnlandTermux-5.13.3.apk`,F-Droid Termux 使用 `AnlandTermux-5.13.3-compatible.apk`。安装完成后,可以**长按应用图标**,进入设置界面。 - 安装完成后,可以**长按应用图标**,进入设置界面。 - -2. 在 Termux 安装守护程序,如 `anland_5.11.0-1_aarch64.deb`。 +2. 在 Termux 安装守护程序,如 `anland_5.13.3_aarch64.deb`。 ```sh - pkg reinstall ./anland_5.11.0-1_aarch64.deb + pkg reinstall ./anland_5.13.3_aarch64.deb ``` > [!TIP] @@ -66,8 +72,10 @@ echo $TERMUX_APP__APK_RELEASE > 使用方法如下。它将会启动 KDE Plasma 或 Weston,然后请切换到 Android 的“Anland Termux”应用。命令中的环境变量 `ANLAND_WESTON_SCALE` 为 Weston 的缩放倍数,请根据实际需要设置为整数。 > > ```sh -> killall anland > /dev/null 2>&1 -> anland > /dev/null 2>&1 & +> # 启动守护程序: +> killall anland > /dev/null 2>&1; anland > /dev/null 2>&1 & +> # Compatible APK 需要额外运行 Binder 桥: +> pkill -TERM -x anland-compatible; anland-compatible & > # 使用 Debian 13 的 KDE Plasma: > proot-distro login debian-anland --shared-tmp -- bash -c "startplasma-anland" > # 使用 Ubuntu 26.04 的 KDE Plasma: @@ -91,7 +99,7 @@ echo $TERMUX_APP__APK_RELEASE ```sh sudo apt reinstall ./xwayland_24.1.6-91_arm64.deb unzip kwin_anland-5.8-debian-4_6.3.6-92.zip -d kwin-debs-install/ - sudo apt reinstall kwin-debs-install/*.deb + sudo apt reinstall ./kwin-debs-install/*.deb rm -rf kwin-debs-install/ ``` @@ -100,7 +108,7 @@ echo $TERMUX_APP__APK_RELEASE ```sh sudo apt reinstall ./xwayland_24.1.10-91_arm64.deb unzip weston_anland-5.13-ubuntu-14.0.2-92.zip -d weston-debs-install/ - sudo apt reinstall weston-debs-install/*.deb + sudo apt reinstall ./weston-debs-install/*.deb rm -rf weston-debs-install/ ``` @@ -153,10 +161,13 @@ echo $TERMUX_APP__APK_RELEASE 1. 在 Termux 启动守护程序: ```sh - killall anland > /dev/null 2>&1 - anland > /dev/null 2>&1 & + killall anland > /dev/null 2>&1; anland > /dev/null 2>&1 & + # Compatible APK 需要额外运行 Binder 桥: + pkill -TERM -x anland-compatible; anland-compatible & ``` + 使用 compatible APK 时,请在显示应用和守护程序运行期间保持 Termux 中的 `anland-compatible` 进程运行。 + 2. 如果实际运行环境是 Linux 容器的话,需要将 Termux 的 `$TMPDIR` 绑定挂载到容器内部的 `/tmp`。 比如 PRoot-Distro 容器在登录时需添加 `--shared-tmp` 选项: @@ -168,7 +179,7 @@ echo $TERMUX_APP__APK_RELEASE 3. 进入实际运行环境后,下载并运行以下一键脚本。 > [!TIP] -> 为了使音频服务正常运行,执行脚本前请先确认 PipeWire 已安装。如 Termux 中的 `pipewire` 包,Debian/Ubuntu 中的 `pipewire-audio` 包。 +> 为了使音频服务正常运行,执行脚本前请先确认 PipeWire 已安装。如 Termux 中的 `pipewire` 包,Debian/Ubuntu 中的 `pipewire-audio` 和 `pipewire-libcamera` 包。 KDE Plasma:[startplasma-anland.sh](../scripts/startplasma-anland.sh) diff --git a/packages/anland/build.sh b/packages/anland/build.sh index b604b81..a884e09 100644 --- a/packages/anland/build.sh +++ b/packages/anland/build.sh @@ -21,4 +21,6 @@ termux_step_make_install() { repo_root="$(realpath "$TERMUX_PKG_BUILDER_DIR/../..")" install -Dm700 anland "$TERMUX_PREFIX/bin/anland" + install -Dm755 "$repo_root/termux/anland/anland-compatible" \ + "$TERMUX_PREFIX/bin/anland-compatible" } diff --git a/termux/anland/Makefile b/termux/anland/Makefile index 0b94fc5..4dd97de 100644 --- a/termux/anland/Makefile +++ b/termux/anland/Makefile @@ -6,6 +6,7 @@ LDLIBS ?= PREFIX ?= /data/data/com.termux/files/usr BINDIR ?= $(PREFIX)/bin +COMPATIBLE_BRIDGE ?= anland-compatible SOURCES = anland.c common/socket_utils.c OBJECTS = $(SOURCES:.c=.o) @@ -23,6 +24,7 @@ anland: $(OBJECTS) install: anland install -d "$(DESTDIR)$(BINDIR)" install -m 0755 anland "$(DESTDIR)$(BINDIR)/anland" + install -m 0755 "$(COMPATIBLE_BRIDGE)" "$(DESTDIR)$(BINDIR)/$(COMPATIBLE_BRIDGE)" clean: rm -f anland $(OBJECTS) diff --git a/termux/anland/README.md b/termux/anland/README.md index f778380..3fb24b4 100644 --- a/termux/anland/README.md +++ b/termux/anland/README.md @@ -23,6 +23,14 @@ anland /custom/path/display_daemon.sock anland --socket /custom/path/display_daemon.sock ``` +The package also installs `anland-compatible`. It starts the compatible APK's +Termux-side Binder bridge, which connects to the daemon as the Termux UID and +passes a duplicate socket fd to the app: + +```sh +anland-compatible +``` + Build inside Termux: ```sh diff --git a/termux/anland/anland-compatible b/termux/anland/anland-compatible new file mode 100755 index 0000000..8ddcf90 --- /dev/null +++ b/termux/anland/anland-compatible @@ -0,0 +1,22 @@ +#!/data/data/com.termux/files/usr/bin/sh +set -eu + +if [ "$#" -gt 1 ] || { [ "$#" -eq 1 ] && [ -z "$1" ]; }; then + echo "usage: anland-compatible [SOCKET_PATH]" >&2 + exit 1 +fi + +socket_path="${1:-${TMPDIR:-/data/data/com.termux/files/usr/tmp}/anland/display_daemon.sock}" + +apk_path="$(/system/bin/pm path com.anland.termux \ + | sed -n 's/^package:\(.*\)$/\1/p' | head -n 1)" +if [ -z "$apk_path" ]; then + echo "Anland Termux APK is not installed" >&2 + exit 1 +fi + +export CLASSPATH="$apk_path" +unset LD_LIBRARY_PATH LD_PRELOAD +exec /system/bin/app_process -Xnoimage-dex2oat / \ + --nice-name=anland-compatible \ + com.anland.termux.CompatibleBridge "$socket_path" diff --git a/tools/build-app.sh b/tools/build-app.sh index d949e50..38e8915 100755 --- a/tools/build-app.sh +++ b/tools/build-app.sh @@ -24,9 +24,9 @@ fi mkdir -p "$OUT_DIR" -(cd "$ROOT_DIR/app" && gradle --no-daemon assembleDebug) +(cd "$ROOT_DIR/app" && gradle --no-daemon assembleStandardDebug) -mapfile -t DEBUG_APKS < <(find "$ROOT_DIR/app/build/outputs/apk/debug" -maxdepth 1 -type f -name "*.apk" | sort) +mapfile -t DEBUG_APKS < <(find "$ROOT_DIR/app/build/outputs/apk/standard/debug" -maxdepth 1 -type f -name "*.apk" | sort) if [[ "${#DEBUG_APKS[@]}" -ne 1 ]]; then echo "Expected exactly one debug APK, found ${#DEBUG_APKS[@]}:" >&2 printf ' %s\n' "${DEBUG_APKS[@]}" >&2 diff --git a/tools/build-compatible-app.sh b/tools/build-compatible-app.sh new file mode 100755 index 0000000..a9544ca --- /dev/null +++ b/tools/build-compatible-app.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT_DIR="$ROOT_DIR/out" +VERSION_NAME="$(awk -F'"' '/^[[:space:]]*versionName[[:space:]]*=/ { print $2; exit }' "$ROOT_DIR/app/build.gradle")" +APK_NAME="AnlandTermux-${VERSION_NAME}-compatible.apk" + +if [[ -z "$VERSION_NAME" ]]; then + echo "Could not read versionName from app/build.gradle" >&2 + exit 1 +fi + +if ! command -v gradle >/dev/null 2>&1; then + echo "gradle must be available on PATH" >&2 + exit 1 +fi + +GRADLE_VERSION="$(gradle --version | awk '/^Gradle / { print $2; exit }')" +if [[ "$GRADLE_VERSION" != "9.6.0" ]]; then + echo "Gradle 9.6.0 is required, found: ${GRADLE_VERSION:-unknown}" >&2 + exit 1 +fi + +mkdir -p "$OUT_DIR" + +(cd "$ROOT_DIR/app" && gradle --no-daemon assembleCompatibleDebug) + +mapfile -t DEBUG_APKS < <(find "$ROOT_DIR/app/build/outputs/apk/compatible/debug" -maxdepth 1 -type f -name "*.apk" | sort) +if [[ "${#DEBUG_APKS[@]}" -ne 1 ]]; then + echo "Expected exactly one compatible debug APK, found ${#DEBUG_APKS[@]}:" >&2 + printf ' %s\n' "${DEBUG_APKS[@]}" >&2 + exit 1 +fi + +cp "${DEBUG_APKS[0]}" "$OUT_DIR/$APK_NAME" + +echo "Built $OUT_DIR/$APK_NAME"