v3.2.0: full QA fixes and in-app virtual environment

This commit is contained in:
admin
2026-05-21 14:10:15 +04:00
Unverified
parent 2e5d2f819a
commit 66354c182b
10 changed files with 290 additions and 95 deletions

View File

@@ -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
}
}

View File

@@ -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));
}

View File

@@ -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 "";
}

View File

@@ -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<String[]> extractBootstrap(File zipFile, String destDir, ExtractCallback callback) throws Exception {
List<String[]> symlinks = new ArrayList<>();
ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile));
ZipInputStream zis;
ZipEntry entry;
int extracted = 0;
int total = 0;
java.util.Enumeration<java.util.zip.ZipEntry> 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();

View File

@@ -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<String, Process> activeProcesses = new HashMap<>();
private final Map<String, Process> 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";
}

View File

@@ -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();