diff --git a/README.md b/README.md index af6bf66..a8ad7ca 100644 --- a/README.md +++ b/README.md @@ -631,6 +631,17 @@ data: [DONE] ## Changelog +### v3.2.0 (2026-05-20) +- **Full Internal Virtual Environment** — `BootstrapPlugin.setupVirtualEnv()` now provisions an app-contained Python venv under app storage, no external Termux app required +- **In-App Module Installation** — `BootstrapPlugin.venvPipInstall()` installs Python modules directly into the internal venv +- **New AI Automation Tags** — `[VENV_SETUP]` and `[VENV_PIP_INSTALL package]` available in coding and agentic modes +- **Critical Runtime Fix** — replaced invalid JS `new File(...).exists` check with shell-based file existence test in PRoot strategy +- **Concurrency Fix** — `ShellPlugin.activeProcesses` migrated to `ConcurrentHashMap` to avoid race/corruption under parallel process events +- **Wake Stability** — added null-safe activity handling and power service guards in `WakePlugin` +- **Bootstrap Resource Safety** — fixed major stream and descriptor leaks in download/extract paths (`downloadFile`, `extractBootstrap`) +- **Accessibility Labels** — added missing `aria-label` attributes for icon-only controls in UI +- **Version sync** — About screen and package/build versions updated to 3.2.0 + ### v3.1.0 (2026-05-20) - **AutoGLM Device Control** — `AccessibilityService` for full device automation: tap, swipe, long press, type text, screenshot, UI tree - **Navigation Controls** — press Back, Home, Recents, Notifications, Quick Settings, Power Dialog via global actions diff --git a/android/app/build.gradle b/android/app/build.gradle index 1411390..8ffe381 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,5 +1,19 @@ apply plugin: 'com.android.application' +def envOrDefault(String key, String fallback) { + def fromEnv = System.getenv(key) + if (fromEnv != null && fromEnv.trim()) return fromEnv.trim() + if (project.hasProperty(key) && project.property(key)?.toString()?.trim()) return project.property(key).toString().trim() + return fallback +} + +def debugStorePass = envOrDefault('ZAI_DEBUG_STORE_PASSWORD', 'android') +def debugKeyAlias = envOrDefault('ZAI_DEBUG_KEY_ALIAS', 'androiddebugkey') +def debugKeyPass = envOrDefault('ZAI_DEBUG_KEY_PASSWORD', 'android') +def releaseStorePass = envOrDefault('ZAI_RELEASE_STORE_PASSWORD', 'zaichat') +def releaseKeyAlias = envOrDefault('ZAI_RELEASE_KEY_ALIAS', 'zai-chat') +def releaseKeyPass = envOrDefault('ZAI_RELEASE_KEY_PASSWORD', 'zaichat') + android { namespace = "ai.z.chat" compileSdk = rootProject.ext.compileSdkVersion @@ -7,8 +21,8 @@ android { applicationId "ai.z.chat" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 22 - versionName "3.1.2" + versionCode 23 + versionName "3.2.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' @@ -18,15 +32,15 @@ android { signingConfigs { debug { storeFile file('debug.keystore') - storePassword 'android' - keyAlias 'androiddebugkey' - keyPassword 'android' + storePassword debugStorePass + keyAlias debugKeyAlias + keyPassword debugKeyPass } release { storeFile file('release.keystore') - storePassword 'zaichat' - keyAlias 'zai-chat' - keyPassword 'zaichat' + storePassword releaseStorePass + keyAlias releaseKeyAlias + keyPassword releaseKeyPass } } diff --git a/android/app/src/main/java/ai/z/chat/AutoGLMPlugin.java b/android/app/src/main/java/ai/z/chat/AutoGLMPlugin.java index 638dd25..21079c7 100644 --- a/android/app/src/main/java/ai/z/chat/AutoGLMPlugin.java +++ b/android/app/src/main/java/ai/z/chat/AutoGLMPlugin.java @@ -9,7 +9,6 @@ import com.getcapacitor.annotation.Permission; import android.content.Intent; import android.provider.Settings; -import android.text.TextUtils; @CapacitorPlugin( name = "AutoGLM", @@ -130,7 +129,11 @@ public class AutoGLMPlugin extends Plugin { destPath = getContext().getCacheDir() + "/autoglm_screenshot.png"; } svc.takeScreenshot(destPath); - try { Thread.sleep(500); } catch (Exception e) {} + try { + Thread.sleep(500); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } call.resolve(new JSObject().put("path", destPath).put("ok", true)); } diff --git a/android/app/src/main/java/ai/z/chat/AutoGLMService.java b/android/app/src/main/java/ai/z/chat/AutoGLMService.java index 75377d7..cfba7e0 100644 --- a/android/app/src/main/java/ai/z/chat/AutoGLMService.java +++ b/android/app/src/main/java/ai/z/chat/AutoGLMService.java @@ -23,7 +23,7 @@ import java.util.List; public class AutoGLMService extends AccessibilityService { private static final String TAG = "AutoGLMService"; - private static AutoGLMService instance; + private static volatile AutoGLMService instance; @Override public void onCreate() { @@ -138,12 +138,14 @@ public class AutoGLMService extends AccessibilityService { screenshot.getHardwareBuffer(), screenshot.getColorSpace()); if (bitmap != null) { Bitmap softwareBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false); - FileOutputStream fos = new FileOutputStream(destPath); - softwareBitmap.compress(Bitmap.CompressFormat.PNG, 90, fos); - fos.close(); + try (FileOutputStream fos = new FileOutputStream(destPath)) { + softwareBitmap.compress(Bitmap.CompressFormat.PNG, 90, fos); + } softwareBitmap.recycle(); } - screenshot.getHardwareBuffer().close(); + if (screenshot.getHardwareBuffer() != null) { + screenshot.getHardwareBuffer().close(); + } } catch (Exception e) { Log.e(TAG, "Screenshot save failed", e); } @@ -194,7 +196,9 @@ public class AutoGLMService extends AccessibilityService { focusNode.recycle(); return obj.toString(); } - } catch (Exception e) {} + } catch (Exception e) { + Log.w(TAG, "getFocusedNodeInfo failed", e); + } return "{}"; } @@ -261,7 +265,9 @@ public class AutoGLMService extends AccessibilityService { } } } - } catch (Exception e) {} + } catch (Exception e) { + Log.w(TAG, "getCurrentApp failed", e); + } return ""; } 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 385c901..74b3675 100644 --- a/android/app/src/main/java/ai/z/chat/BootstrapPlugin.java +++ b/android/app/src/main/java/ai/z/chat/BootstrapPlugin.java @@ -59,7 +59,7 @@ public class BootstrapPlugin extends Plugin { private String stagingDir; private String homeDir; private String binDir; - private boolean isInstalling = false; + private volatile boolean isInstalling = false; @Override public void load() { @@ -203,39 +203,35 @@ public class BootstrapPlugin extends Plugin { conn.connect(); int total = conn.getContentLength(); - BufferedInputStream in = new BufferedInputStream(conn.getInputStream()); - FileOutputStream out = new FileOutputStream(outputFile); + try (BufferedInputStream in = new BufferedInputStream(conn.getInputStream()); + FileOutputStream out = new FileOutputStream(outputFile)) { + byte[] buffer = new byte[BUFFER_SIZE]; + long downloaded = 0; + int read; + long lastNotify = 0; - byte[] buffer = new byte[BUFFER_SIZE]; - long downloaded = 0; - int read; - long lastNotify = 0; - - while ((read = in.read(buffer)) != -1) { - out.write(buffer, 0, read); - downloaded += read; - long now = System.currentTimeMillis(); - if (callback != null && now - lastNotify > 500) { - callback.onProgress(downloaded, total); - lastNotify = now; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + downloaded += read; + long now = System.currentTimeMillis(); + if (callback != null && now - lastNotify > 500) { + callback.onProgress(downloaded, total); + lastNotify = now; + } } + out.flush(); + } finally { + conn.disconnect(); } - - out.flush(); - out.close(); - in.close(); - conn.disconnect(); } private List extractBootstrap(File zipFile, String destDir, ExtractCallback callback) throws Exception { List symlinks = new ArrayList<>(); - ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile)); + ZipInputStream zis; ZipEntry entry; int extracted = 0; int total = 0; - java.util.Enumeration entries = java.util.Collections.emptyEnumeration(); - java.util.zip.ZipFile zf = new java.util.zip.ZipFile(zipFile); total = zf.size(); zf.close(); @@ -261,13 +257,13 @@ public class BootstrapPlugin extends Plugin { } else { File parent = outFile.getParentFile(); if (parent != null) parent.mkdirs(); - FileOutputStream fos = new FileOutputStream(outFile); - byte[] buf = new byte[BUFFER_SIZE]; - int len; - while ((len = zis.read(buf)) > 0) { - fos.write(buf, 0, len); + try (FileOutputStream fos = new FileOutputStream(outFile)) { + byte[] buf = new byte[BUFFER_SIZE]; + int len; + while ((len = zis.read(buf)) > 0) { + fos.write(buf, 0, len); + } } - fos.close(); } } extracted++; @@ -726,6 +722,93 @@ public class BootstrapPlugin extends Plugin { }).start(); } + @PluginMethod + public void setupVirtualEnv(PluginCall call) { + call.setKeepAlive(true); + new Thread(() -> { + try { + String prefix = call.getString("prefix", prefixDir); + String home = call.getString("home", homeDir + "/home"); + String venvDir = call.getString("venv", filesDir + "/venv/default"); + + new File(venvDir).getParentFile().mkdirs(); + String pkgBin = prefix + "/bin/pkg"; + String aptBin = prefix + "/bin/apt"; + String python3 = prefix + "/bin/python3"; + + if (!new File(pkgBin).exists() && !new File(aptBin).exists()) { + call.reject("Bootstrap tools missing. Install internal dev environment first."); + return; + } + + if (!new File(python3).exists()) { + String installer = new File(pkgBin).exists() ? pkgBin : aptBin; + runBootstrapCommand(prefix, home, + "sh \"" + installer + "\" install -y python clang rust make pkg-config libffi openssl"); + } + + if (!new File(python3).exists()) { + call.reject("python3 unavailable after install"); + return; + } + + runBootstrapCommand(prefix, home, + "\"" + python3 + "\" -m venv \"" + venvDir + "\" && " + + "\"" + venvDir + "/bin/pip\" install --upgrade pip setuptools wheel"); + + call.resolve(new JSObject() + .put("ok", true) + .put("venv", venvDir) + .put("python", venvDir + "/bin/python") + .put("pip", venvDir + "/bin/pip")); + } catch (Exception e) { + try { call.reject("setupVirtualEnv failed: " + e.getMessage()); } catch (Exception ignored) {} + } + }).start(); + } + + @PluginMethod + public void venvPipInstall(PluginCall call) { + call.setKeepAlive(true); + new Thread(() -> { + try { + String prefix = call.getString("prefix", prefixDir); + String home = call.getString("home", homeDir + "/home"); + String venvDir = call.getString("venv", filesDir + "/venv/default"); + String packages = call.getString("packages", ""); + if (packages.trim().isEmpty()) { + call.reject("packages required"); + return; + } + + String pip = venvDir + "/bin/pip"; + if (!new File(pip).exists()) { + call.reject("venv pip not found. Run setupVirtualEnv first."); + return; + } + + String output = runBootstrapCommand(prefix, home, + "\"" + pip + "\" install " + packages); + call.resolve(new JSObject().put("ok", true).put("output", output)); + } catch (Exception e) { + try { call.reject("venvPipInstall failed: " + e.getMessage()); } catch (Exception ignored) {} + } + }).start(); + } + + private String runBootstrapCommand(String prefix, String home, String command) throws Exception { + ProcessBuilder pb = new ProcessBuilder("/system/bin/sh", "-c", + "export PREFIX=\"" + prefix + "\" HOME=\"" + home + "\" " + + "LD_LIBRARY_PATH=\"" + prefix + "/lib\" PATH=\"" + prefix + "/bin:/system/bin:$PATH\" " + + "ANDROID_API_LEVEL=\"" + android.os.Build.VERSION.SDK_INT + "\" && " + command); + pb.redirectErrorStream(true); + Process p = pb.start(); + String out = drainProcess(p); + int code = p.waitFor(); + if (code != 0) throw new RuntimeException("cmd failed(" + code + "): " + out); + return out; + } + private String drainProcess(Process p) throws Exception { java.io.InputStream is = p.getInputStream(); java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); 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 874f787..e65138f 100644 --- a/android/app/src/main/java/ai/z/chat/ShellPlugin.java +++ b/android/app/src/main/java/ai/z/chat/ShellPlugin.java @@ -13,10 +13,9 @@ import com.getcapacitor.annotation.Permission; import java.io.BufferedReader; import java.io.File; import java.io.InputStreamReader; -import java.io.InputStream; -import java.io.OutputStream; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; @CapacitorPlugin(name = "Shell", permissions = { @@ -25,7 +24,7 @@ import java.util.Map; ) public class ShellPlugin extends Plugin { private static final String TAG = "ShellPlugin"; - private final Map activeProcesses = new HashMap<>(); + private final Map activeProcesses = new ConcurrentHashMap<>(); private String currentCwd = null; private String homeDir = null; private String toolsDir = null; @@ -228,9 +227,9 @@ public class ShellPlugin extends Plugin { File file = new File(path); File parent = file.getParentFile(); if (parent != null && !parent.exists()) parent.mkdirs(); - java.io.FileWriter writer = new java.io.FileWriter(file); - writer.write(content); - writer.close(); + try (java.io.FileWriter writer = new java.io.FileWriter(file)) { + writer.write(content); + } call.resolve(new JSObject().put("path", file.getAbsolutePath()).put("size", file.length())); } catch (Exception e) { call.reject("Write failed: " + e.getMessage()); @@ -250,13 +249,13 @@ public class ShellPlugin extends Plugin { call.reject("File not found: " + path); return; } - BufferedReader reader = new BufferedReader(new InputStreamReader(new java.io.FileInputStream(file), "UTF-8")); StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line).append("\n"); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(new java.io.FileInputStream(file), "UTF-8"))) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } } - reader.close(); call.resolve(new JSObject().put("content", sb.toString()).put("path", file.getAbsolutePath())); } catch (Exception e) { call.reject("Read failed: " + e.getMessage()); @@ -367,12 +366,15 @@ public class ShellPlugin extends Plugin { try { File versionFile = new File("/data/data/com.termux/files/usr/share/doc/termux/VERSION"); if (versionFile.exists()) { - java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(versionFile)); - String version = reader.readLine(); - reader.close(); + String version; + try (java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(versionFile))) { + version = reader.readLine(); + } return version != null ? version : "unknown"; } - } catch (Exception e) {} + } catch (Exception e) { + Log.w(TAG, "Unable to read Termux version", e); + } return "installed"; } diff --git a/android/app/src/main/java/ai/z/chat/WakePlugin.java b/android/app/src/main/java/ai/z/chat/WakePlugin.java index 9aacbd8..269172c 100644 --- a/android/app/src/main/java/ai/z/chat/WakePlugin.java +++ b/android/app/src/main/java/ai/z/chat/WakePlugin.java @@ -14,7 +14,7 @@ import com.getcapacitor.annotation.CapacitorPlugin; public class WakePlugin extends Plugin { private PowerManager.WakeLock screenWakeLock; private PowerManager.WakeLock cpuWakeLock; - private boolean isHeld = false; + private volatile boolean isHeld = false; @Override public void load() { @@ -29,11 +29,19 @@ public class WakePlugin extends Plugin { } try { - getActivity().runOnUiThread(() -> { - getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - }); + if (getActivity() != null) { + getActivity().runOnUiThread(() -> { + if (getActivity() != null) { + getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + }); + } PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE); + if (pm == null) { + call.reject("Power service unavailable"); + return; + } screenWakeLock = pm.newWakeLock( PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, @@ -57,9 +65,13 @@ public class WakePlugin extends Plugin { @PluginMethod public void release(PluginCall call) { try { - getActivity().runOnUiThread(() -> { - getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - }); + if (getActivity() != null) { + getActivity().runOnUiThread(() -> { + if (getActivity() != null) { + getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + }); + } if (screenWakeLock != null && screenWakeLock.isHeld()) { screenWakeLock.release(); diff --git a/package.json b/package.json index 15178c8..380c878 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zai-chat", - "version": "3.1.2", + "version": "3.2.0", "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 a6e4784..2377428 100644 --- a/www/index.html +++ b/www/index.html @@ -69,24 +69,24 @@
- +

New Chat

Chat
- - - - + + + +