v3.3.0: File Manager, SSH/Remote access, Approval gate

- File Manager: browse device files, open/preview, install APKs
- SSH/Remote: [SSH_EXEC], [SSH_UPLOAD], [SSH_DOWNLOAD], [REMOTE_EXEC], [CURL_EXEC]
- Approval gate: all sensitive actions require user approval
- New FileManagerPlugin native plugin
- Updated agentic system prompt with external access docs
- Cleaned up stale .idsig artifacts from releases/
This commit is contained in:
admin
2026-05-21 17:42:22 +04:00
Unverified
parent d98508dafa
commit 5125725ea7
10 changed files with 565 additions and 66 deletions

View File

@@ -21,8 +21,8 @@ android {
applicationId "ai.z.chat"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 23
versionName "3.2.0"
versionCode 25
versionName "3.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'

View File

@@ -0,0 +1,160 @@
package ai.z.chat;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.webkit.MimeTypeMap;
import androidx.core.content.FileProvider;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@CapacitorPlugin(name = "FileManager")
public class FileManagerPlugin extends Plugin {
@PluginMethod
public void openFile(PluginCall call) {
String path = call.getString("path", "");
String mimeType = call.getString("mimeType", "");
if (path.isEmpty()) {
call.reject("No path provided");
return;
}
File file = new File(path);
if (!file.exists()) {
call.reject("File not found: " + path);
return;
}
try {
Context context = getContext();
Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
String resolvedMime = mimeType.isEmpty() ? guessMimeType(file.getName()) : mimeType;
if (resolvedMime == null || resolvedMime.isEmpty()) resolvedMime = "*/*";
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, resolvedMime);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
call.resolve(new JSObject().put("opened", true).put("path", file.getAbsolutePath()).put("mimeType", resolvedMime));
} catch (Exception e) {
call.reject("Open failed: " + e.getMessage());
}
}
@PluginMethod
public void listFiles(PluginCall call) {
String rootPath = call.getString("path", "");
if (rootPath.isEmpty()) {
call.reject("No path provided");
return;
}
File root = new File(rootPath);
if (!root.exists()) {
call.reject("Path not found: " + rootPath);
return;
}
JSObject result = new JSObject();
result.put("path", root.getAbsolutePath());
result.put("name", root.getName());
result.put("directory", root.isDirectory());
result.put("size", root.isFile() ? root.length() : 0);
result.put("mimeType", root.isFile() ? guessMimeType(root.getName()) : "inode/directory");
if (root.isDirectory()) {
File[] children = root.listFiles();
List<JSObject> items = new ArrayList<>();
if (children != null) {
Arrays.sort(children, (a, b) -> a.getName().compareToIgnoreCase(b.getName()));
for (File child : children) {
JSObject item = new JSObject();
item.put("name", child.getName());
item.put("path", child.getAbsolutePath());
item.put("directory", child.isDirectory());
item.put("size", child.isFile() ? child.length() : 0);
item.put("mimeType", child.isFile() ? guessMimeType(child.getName()) : "inode/directory");
items.add(item);
}
}
result.put("items", items);
}
call.resolve(result);
}
@PluginMethod
public void openContainingFolder(PluginCall call) {
String path = call.getString("path", "");
if (path.isEmpty()) {
call.reject("No path provided");
return;
}
File file = new File(path);
File target = file.isDirectory() ? file : file.getParentFile();
if (target == null || !target.exists()) {
call.reject("Containing folder not found");
return;
}
call.resolve(new JSObject().put("opened", true).put("path", target.getAbsolutePath()));
}
@PluginMethod
public void getRoots(PluginCall call) {
JSObject result = new JSObject();
List<JSObject> roots = new ArrayList<>();
roots.add(rootItem("App Files", getContext().getFilesDir()));
File ext = getContext().getExternalFilesDir(null);
if (ext != null) roots.add(rootItem("External Files", ext));
File downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
if (downloads != null) roots.add(rootItem("Downloads", downloads));
File dcim = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
if (dcim != null) roots.add(rootItem("Camera", dcim));
File sdcard = Environment.getExternalStorageDirectory();
if (sdcard != null) roots.add(rootItem("Storage Root", sdcard));
result.put("roots", roots);
call.resolve(result);
}
private JSObject rootItem(String label, File file) {
JSObject item = new JSObject();
item.put("name", label);
item.put("path", file.getAbsolutePath());
item.put("directory", true);
item.put("size", 0);
item.put("mimeType", "inode/directory");
return item;
}
private String guessMimeType(String name) {
String lower = name.toLowerCase();
if (lower.endsWith(".apk")) return "application/vnd.android.package-archive";
if (lower.endsWith(".apks")) return "application/vnd.android.package-archive";
if (lower.endsWith(".zip")) return "application/zip";
if (lower.endsWith(".json")) return "application/json";
if (lower.endsWith(".html") || lower.endsWith(".htm")) return "text/html";
if (lower.endsWith(".md") || lower.endsWith(".txt") || lower.endsWith(".log")) return "text/plain";
String ext = MimeTypeMap.getFileExtensionFromUrl(name);
if (ext == null || ext.isEmpty()) return "*/*";
String guess = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.toLowerCase());
return guess != null ? guess : "*/*";
}
}

View File

@@ -15,6 +15,9 @@ import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
@CapacitorPlugin(name = "Installer")
public class InstallerPlugin extends Plugin {
@@ -58,6 +61,45 @@ public class InstallerPlugin extends Plugin {
}
}
@PluginMethod
public void exportApk(PluginCall call) {
String path = call.getString("path", "");
String name = call.getString("name", "app.apk");
if (path.isEmpty()) {
call.reject("No path provided");
return;
}
File apkFile = new File(path);
if (!apkFile.exists()) {
call.reject("APK file not found: " + path);
return;
}
try {
Context context = getContext();
File outRoot = context.getExternalFilesDir(android.os.Environment.DIRECTORY_DOWNLOADS);
if (outRoot == null) {
outRoot = context.getFilesDir();
}
File outDir = new File(outRoot, "ZAI-Chat");
if (!outDir.exists()) outDir.mkdirs();
File outFile = new File(outDir, name);
try (FileInputStream fis = new FileInputStream(apkFile);
FileOutputStream fos = new FileOutputStream(outFile)) {
byte[] buf = new byte[8192];
int r;
while ((r = fis.read(buf)) > 0) fos.write(buf, 0, r);
}
call.resolve(new JSObject()
.put("exported", true)
.put("path", outFile.getAbsolutePath())
.put("name", name));
} catch (Exception e) {
call.reject("Export failed: " + e.getMessage());
}
}
@PluginMethod
public void getDeviceInfo(PluginCall call) {
JSObject info = new JSObject();

View File

@@ -9,6 +9,7 @@ public class MainActivity extends BridgeActivity {
protected void onCreate(Bundle savedInstanceState) {
registerPlugin(ShellPlugin.class);
registerPlugin(InstallerPlugin.class);
registerPlugin(FileManagerPlugin.class);
registerPlugin(WakePlugin.class);
registerPlugin(BootstrapPlugin.class);
registerPlugin(AutoGLMPlugin.class);