Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions .github/workflows/build-anland-apk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
- termux
paths:
- "app/**"
- "packages/anland/**"
- "termux/**"
- "tools/**"
- ".github/workflows/build-anland-apk.yml"
workflow_dispatch:
inputs:
ref:
Expand Down Expand Up @@ -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 }}
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<version>.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-<version>-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).
Expand Down
5 changes: 5 additions & 0 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<version>.apk`。它使用 `sharedUserId` 且作为 Termux 的一部分运行,因此 Android 会一直将它们视为同一个应用,而且不会降低其速度。
* 如果 Termux 来自 [F-Droid](https://f-droid.org/packages/com.termux/) 或 ZeroTermux 等变体,请改用 `AnlandTermux-<version>-compatible.apk`。它不使用 `sharedUserId`,而是由 Termux 侧的 `anland-compatible` bridge 持有守护程序 socket,再通过 Binder 传递 fd。详情请参阅[用户指南](docs/user-guide_zh.md)。

## 用户指南

Anland: Termux 的安装和使用说明请参见:[Anland:Termux 用户指南](docs/user-guide_zh.md)
Expand Down
14 changes: 14 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ android {

buildFeatures {
buildConfig = true
aidl = true
}

signingConfigs {
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions app/src/compatible/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
3 changes: 1 addition & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:sharedUserId="com.termux">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/aidl/com/anland/termux/ICompatibleBridge.aidl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.anland.termux;

import android.os.ParcelFileDescriptor;

interface ICompatibleBridge {
ParcelFileDescriptor getConnection();
}
147 changes: 147 additions & 0 deletions app/src/main/java/com/anland/termux/CompatibleBridge.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading