diff --git a/android/app/build.gradle b/android/app/build.gradle index 3dcf654..6c7d76d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "ai.z.chat" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 17 - versionName "2.2.4" + versionCode 18 + versionName "2.2.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' diff --git a/android/app/src/main/java/ai/z/chat/BootstrapPlugin.java b/android/app/src/main/java/ai/z/chat/BootstrapPlugin.java index 43782e6..b63d9b3 100644 --- a/android/app/src/main/java/ai/z/chat/BootstrapPlugin.java +++ b/android/app/src/main/java/ai/z/chat/BootstrapPlugin.java @@ -1,6 +1,9 @@ package ai.z.chat; import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; import android.util.Log; import com.getcapacitor.JSObject; @@ -274,12 +277,16 @@ public class BootstrapPlugin extends Plugin { private void patchPaths(String dir) { try { - ProcessBuilder pb = new ProcessBuilder("find", dir, "-type", "f", - "(", "-name", "*.sh", "-o", "-name", "*.conf", "-o", "-name", "*.cfg", - "-o", "-name", "*.txt", "-o", "-name", "*.env", "-o", "-name", "properties.sh", - "-o", "-name", "profile", "-o", "-name", "bashrc", "-o", "-name", "*.profile", ")"); - pb.redirectErrorStream(true); - Process p = pb.start(); + String ourPrefix = filesDir + "/usr"; + String sedExpr = "s|/data/data/com.termux/files/usr|" + ourPrefix + "|g;" + + "s|/data/data/com.termux/files/home|" + homeDir + "|g;" + + "s|/data/data/com.termux|" + filesDir + "|g"; + + ProcessBuilder findPb = new ProcessBuilder("find", dir, "-type", "f", + "-not", "-name", "*.so", "-not", "-name", "*.elf", + "-not", "-name", "*.pyc", "-not", "-name", "*.a", "-size", "-512k"); + findPb.redirectErrorStream(true); + Process p = findPb.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); List files = new ArrayList<>(); String line; @@ -288,20 +295,20 @@ public class BootstrapPlugin extends Plugin { } p.waitFor(); - String ourPrefix = filesDir + "/usr"; for (String filePath : files) { try { - ProcessBuilder sedPb = new ProcessBuilder("sed", "-i", - "s|/data/data/com.termux/files/usr|" + ourPrefix + "|g;" + - "s|/data/data/com.termux/files/home|" + homeDir + "|g;" + - "s|/data/data/com.termux|" + filesDir + "|g", - filePath); + ProcessBuilder sedPb = new ProcessBuilder("sed", "-i", sedExpr, filePath); sedPb.start().waitFor(); } catch (Exception e) { Log.w(TAG, "Patch failed for " + filePath + ": " + e.getMessage()); } } + patchShebangs(dir + "/bin"); + if (new File(dir + "/libexec").exists()) { + patchShebangs(dir + "/libexec"); + } + ProcessBuilder ldSo = new ProcessBuilder("sed", "-i", "s|/data/data/com.termux/files/usr|" + ourPrefix + "|g", dir + "/etc/ld.so.conf"); @@ -311,6 +318,34 @@ public class BootstrapPlugin extends Plugin { } } + private void patchShebangs(String dirPath) { + File dir = new File(dirPath); + if (!dir.exists() || !dir.isDirectory()) return; + File[] files = dir.listFiles(); + if (files == null) return; + for (File f : files) { + if (f.isDirectory()) continue; + try { + byte[] header = new byte[256]; + java.io.FileInputStream fis = new java.io.FileInputStream(f); + int read = fis.read(header); + fis.close(); + if (read < 2 || header[0] != '#' || header[1] != '!') continue; + + String shebangLine = new String(header, 0, Math.min(read, 256)).split("\n")[0]; + String newShebang = shebangLine.replaceAll("/data/data/com\\.termux/files/usr/bin/(sh|bash)", "/system/bin/sh") + .replaceAll("/data/user/0/ai\\.z\\.chat/files/usr/bin/(sh|bash)", "/system/bin/sh"); + if (!newShebang.equals(shebangLine)) { + ProcessBuilder sedPb = new ProcessBuilder("sed", "-i", "1s|" + shebangLine + "|" + newShebang + "|", f.getAbsolutePath()); + sedPb.start().waitFor(); + Log.i(TAG, "Patched shebang: " + f.getName() + " → " + newShebang); + } + } catch (Exception e) { + Log.w(TAG, "Shebang patch failed for " + f.getName() + ": " + e.getMessage()); + } + } + } + private void chmodRecursive(File dir) { if (!dir.exists() || !dir.isDirectory()) return; File[] children = dir.listFiles(); @@ -337,12 +372,98 @@ public class BootstrapPlugin extends Plugin { chmodRecursive(new File(prefixDir + "/lib")); File etcDir = new File(prefixDir + "/etc"); if (etcDir.exists()) chmodRecursive(etcDir); + patchShebangs(prefixDir + "/bin"); call.resolve(new JSObject().put("fixed", true)); } catch (Exception e) { call.reject("Fix permissions failed: " + e.getMessage()); } } + @PluginMethod + public void isTermuxInstalled(PluginCall call) { + try { + getContext().getPackageManager().getPackageInfo("com.termux", 0); + call.resolve(new JSObject().put("installed", true).put("prefix", "/data/data/com.termux/files/usr")); + } catch (PackageManager.NameNotFoundException e) { + call.resolve(new JSObject().put("installed", false).put("prefix", "")); + } catch (Exception e) { + call.resolve(new JSObject().put("installed", false).put("error", e.getMessage())); + } + } + + @PluginMethod + public void openTermuxPage(PluginCall call) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://f-droid.org/en/packages/com.termux/")); + getContext().startActivity(intent); + call.resolve(new JSObject().put("opened", true)); + } catch (Exception e) { + call.reject("Failed to open: " + e.getMessage()); + } + } + + @PluginMethod + public void runInTermux(PluginCall call) { + String command = call.getString("command", ""); + if (command.isEmpty()) { + call.reject("No command"); + return; + } + try { + String[] cmd = new String[]{ + "/system/bin/am", "broadcast", + "--user", "0", + "-n", "com.termux/com.termux.app.TermuxOpenReceiver", + "-a", "com.termux.RUN_COMMAND", + "--es", "com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/bash", + "--es", "com.termux.RUN_COMMAND_ARGUMENTS", "-c " + command, + "--ez", "com.termux.RUN_COMMAND_BACKGROUND", "false", + "--es", "com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home" + }; + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.redirectErrorStream(true); + Process p = pb.start(); + BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); + StringBuilder output = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) output.append(line).append("\n"); + p.waitFor(10, java.util.concurrent.TimeUnit.SECONDS); + call.resolve(new JSObject().put("sent", true).put("output", output.toString())); + } catch (Exception e) { + call.reject("Termux command failed: " + e.getMessage()); + } + } + + @PluginMethod + public void downloadTermuxApk(PluginCall call) { + call.setKeepAlive(true); + new Thread(() -> { + try { + String packagesContent = downloadToString("https://f-droid.org/repo/index-v1.jar"); + String apkUrl = null; + String[] lines = packagesContent.split("\n"); + for (String l : lines) { + if (l.contains("com.termux") && l.endsWith(".apk")) { + apkUrl = l.trim(); + break; + } + } + + if (apkUrl == null) { + apkUrl = "https://f-droid.org/repo/com.termux_1020.apk"; + } + + File apkFile = new File(getContext().getCacheDir(), "termux.apk"); + downloadFile(apkUrl, apkFile, null); + + call.resolve(new JSObject().put("path", apkFile.getAbsolutePath()) + .put("size", apkFile.length())); + } catch (Exception e) { + try { call.reject("Download failed: " + e.getMessage()); } catch (Exception ignored) {} + } + }).start(); + } + private void writeEnvFile() { try { File envFile = new File(prefixDir + "/etc/termux.env"); diff --git a/android/app/src/main/java/ai/z/chat/ShellPlugin.java b/android/app/src/main/java/ai/z/chat/ShellPlugin.java index 34c6ffc..6e2e1f6 100644 --- a/android/app/src/main/java/ai/z/chat/ShellPlugin.java +++ b/android/app/src/main/java/ai/z/chat/ShellPlugin.java @@ -54,36 +54,23 @@ public class ShellPlugin extends Plugin { private void refreshShell() { String nativeLibDir = getNativeLibDir(); - File prootInPrefix = new File(prefixDir + "/bin/proot"); - if (!prootInPrefix.exists() && nativeLibDir != null) { + if (nativeLibDir != null) { File bundledProot = new File(nativeLibDir, "libproot.so"); File bundledLoader = new File(nativeLibDir, "libproot-loader.so"); if (bundledProot.exists()) { - try { - new File(prefixDir + "/bin").mkdirs(); - new File(prefixDir + "/libexec").mkdirs(); - new File(prefixDir + "/libexec/proot").mkdirs(); - copyFile(bundledProot, new File(prefixDir + "/bin/proot")); - Os.chmod(prefixDir + "/bin/proot", 0755); - if (bundledLoader.exists()) { - copyFile(bundledLoader, new File(prefixDir + "/libexec/proot/loader")); - Os.chmod(prefixDir + "/libexec/proot/loader", 0755); - File loader32 = new File(nativeLibDir, "libproot-loader32.so"); - if (loader32.exists()) { - copyFile(loader32, new File(prefixDir + "/libexec/proot/loader32")); - Os.chmod(prefixDir + "/libexec/proot/loader32", 0755); - } - } - prootPath = prefixDir + "/bin/proot"; - prootLoaderPath = prefixDir + "/libexec/proot/loader"; - Log.i(TAG, "Installed bundled proot from APK: " + prootPath); - } catch (Exception e) { - Log.w(TAG, "Failed to install bundled proot: " + e.getMessage()); + prootPath = bundledProot.getAbsolutePath(); + if (bundledLoader.exists()) { + prootLoaderPath = bundledLoader.getAbsolutePath(); } + Log.i(TAG, "Using bundled proot from nativeLib: " + prootPath); + } + } + + if (prootPath == null) { + File prootInPrefix = new File(prefixDir + "/bin/proot"); + if (prootInPrefix.exists()) { + prootPath = prootInPrefix.getAbsolutePath(); } - } else if (prootInPrefix.exists()) { - try { Os.chmod(prootInPrefix.getAbsolutePath(), 0755); } catch (Exception e) {} - prootPath = prootInPrefix.getAbsolutePath(); } File bash = new File(prefixDir + "/bin/bash"); @@ -98,16 +85,6 @@ public class ShellPlugin extends Plugin { shellPath = "/system/bin/sh"; } - private void copyFile(File src, File dst) throws Exception { - java.io.FileInputStream fis = new java.io.FileInputStream(src); - java.io.FileOutputStream fos = new java.io.FileOutputStream(dst); - byte[] buf = new byte[8192]; - int r; - while ((r = fis.read(buf)) > 0) fos.write(buf, 0, r); - fos.close(); - fis.close(); - } - private String getNativeLibDir() { try { return getContext().getApplicationInfo().nativeLibraryDir; diff --git a/package.json b/package.json index f572991..bc1c923 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zai-chat", - "version": "2.2.4", + "version": "2.2.5", "description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan", "main": "index.js", "scripts": { diff --git a/www/index.html b/www/index.html index 56d5ecd..3752253 100644 --- a/www/index.html +++ b/www/index.html @@ -327,13 +327,24 @@

About

-

Z.AI Chat v2.2.4

+

Z.AI Chat v2.2.5

Built with Z.AI SDK & GLM-5.1

Compatible with Android 15/16

Changelog