v3.2.0: full QA fixes and in-app virtual environment
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user