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:
@@ -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:!*~'
|
||||
|
||||
160
android/app/src/main/java/ai/z/chat/FileManagerPlugin.java
Normal file
160
android/app/src/main/java/ai/z/chat/FileManagerPlugin.java
Normal 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 : "*/*";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user